meocord 4.1.2 → 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 (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-BywwkU9O.cjs');
3
+ var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
4
4
  require('reflect-metadata');
5
5
  var node_async_hooks = require('node:async_hooks');
6
6
  var inversify = require('inversify');
@@ -101,7 +101,7 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
101
101
 
102
102
  function eventDecorator(event, once) {
103
103
  // Generic over the method, so it is checked against the event's arguments rather than required to match exactly
104
- return function(target, propertyKey, _descriptor) {
104
+ return controller_decorator.declaring(function(target, propertyKey, _descriptor) {
105
105
  controller_decorator.refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
106
106
  const handlers = controller_decorator.ownHandlerList(controller_decorator.META.eventHandlers, target);
107
107
  handlers.push({
@@ -110,7 +110,7 @@ function eventDecorator(event, once) {
110
110
  once
111
111
  });
112
112
  Reflect.defineMetadata(controller_decorator.META.eventHandlers, handlers, target);
113
- };
113
+ });
114
114
  }
115
115
  /**
116
116
  * Handles a discord.js client event every time it is emitted, on a controller or a service.
@@ -138,7 +138,7 @@ function eventDecorator(event, once) {
138
138
  * @group Decorators
139
139
  * @category Handlers
140
140
  * @see {@link Once}
141
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
141
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
142
142
  */ function On(event) {
143
143
  return eventDecorator(event, false);
144
144
  }
@@ -165,7 +165,7 @@ function eventDecorator(event, once) {
165
165
  * @group Decorators
166
166
  * @category Handlers
167
167
  * @see {@link On}
168
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
168
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
169
169
  */ function Once(event) {
170
170
  return eventDecorator(event, true);
171
171
  }
@@ -505,14 +505,27 @@ const wrapperCount = perHandler((prototype, methodName)=>{
505
505
  else marks.delete(methodName);
506
506
  return true;
507
507
  }
508
+ /** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
509
+ /**
510
+ * Records each instance `container` makes of `classes` as its own. Two testing modules made from one class each run a
511
+ * direct call on their own instance in themselves, where the class's metadata names only the last.
512
+ */ function recordInstanceContainers(container, classes) {
513
+ for (const cls of classes){
514
+ container.onActivation(cls, (_context, instance)=>{
515
+ if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
516
+ return instance;
517
+ });
518
+ }
519
+ }
508
520
  /**
509
521
  * Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then the
510
522
  * call with a pass for each wrapper inside the one entered, so no guard runs twice.
511
523
  */ async function runDirectCall(instance, methodName, args, call) {
512
524
  const prototype = Object.getPrototypeOf(instance);
513
525
  const controller = instance.constructor;
514
- // The app that made the class, or a class it extends: a subclass the app's code makes runs as its base does
515
- const container = Reflect.getMetadata(controller_decorator.META.container, controller);
526
+ // The container that made the instance, or else the app that made the class or a class it extends: a subclass the
527
+ // app's code makes runs as its base does
528
+ const container = instanceContainers.get(instance) ?? Reflect.getMetadata(controller_decorator.META.container, controller);
516
529
  if (!container) {
517
530
  throw new Error(`${controller.name}.${methodName}: @UseGuard runs its guards through the app that made ${controller.name}, and this ` + `instance was made outside one. Inject ${controller.name}, or call it on an instance the app made.`);
518
531
  }
@@ -767,19 +780,37 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
767
780
  const value = by ? await by(context, params) : undefined;
768
781
  // Encoded, so a value holding a colon cannot count under another value's key. Without a value, `:by` alone keeps
769
782
  // the call apart from a cooldown with no `by` over the same window.
770
- const suffix = !by ? '' : value === undefined ? ':by' : `:by:${encodeURIComponent(String(value))}`;
783
+ const suffix = !by ? '' : value === undefined ? ':by' : `:by:${encodeKeyPart(String(value))}`;
784
+ const scope = scopeId(per, first);
785
+ // The caller, for a notice of a refusal under a scope that counts more than one caller
786
+ const user = scope.startsWith('user:') ? undefined : caller(first).user;
771
787
  counted.push({
772
- key: `${controller.name}.${methodName}#${id}:${per}:${scopeId(per, first)}${suffix}`,
788
+ key: `${controller.name}.${methodName}#${id}:${per}:${scope}${suffix}`,
773
789
  windowMs,
774
790
  uses,
775
- per
791
+ per,
792
+ user
776
793
  });
777
794
  }
778
795
  return counted;
779
796
  }
797
+ // A surrogate with no partner beside it, which encodeURIComponent refuses
798
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
799
+ /**
800
+ * A value as part of a store key: each well-formed run as `encodeURIComponent` writes it, and each lone surrogate as `%u`
801
+ * and its four hex digits, which no other value encodes to, since `encodeURIComponent` writes `%` only before two.
802
+ */ function encodeKeyPart(value) {
803
+ let encoded = '';
804
+ let from = 0;
805
+ for (const { 0: surrogate, index } of value.matchAll(LONE_SURROGATE)){
806
+ encoded += `${encodeURIComponent(value.slice(from, index))}%u${surrogate.charCodeAt(0).toString(16).toUpperCase()}`;
807
+ from = index + 1;
808
+ }
809
+ return encoded + encodeURIComponent(value.slice(from));
810
+ }
780
811
  /**
781
- * The store key and window of the cooldown each refusal came from, and when its wait ends by the store's clock if
782
- * the store said, for {@link claimCooldownNotice}.
812
+ * The store key and window of the cooldown each refusal came from, when its wait ends by the store's clock if the
813
+ * store said, and the refused caller when the key counts more than one, for {@link claimCooldownNotice}.
783
814
  */ const refusalKeys = new WeakMap();
784
815
  /** Asks the store about the call under the app's policy, and throws what the answer means for it. */ async function ask(container, counted, peek, call) {
785
816
  const store = cooldownStoreOf(container);
@@ -812,7 +843,8 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
812
843
  refusalKeys.set(refusal, {
813
844
  key: blocking.key,
814
845
  windowMs: blocking.windowMs,
815
- endsAt: verdict.retryTimestamp
846
+ endsAt: verdict.retryTimestamp,
847
+ user: blocking.user
816
848
  });
817
849
  throw refusal;
818
850
  }
@@ -835,16 +867,18 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
835
867
  // retry landing a moment late is refused on the earlier, which its wait holds, before taking the later, which a wait
836
868
  // ending then needs for its own notice.
837
869
  const bucket = Math.floor(refusal.retryAt.getTime() / NOTICE_BUCKET_MS);
870
+ // A key counting a server, a channel or everyone is shared by every caller it refuses, each told once
871
+ const key = refused.user === undefined ? refused.key : `${refused.key}:user:${refused.user}`;
838
872
  const notices = refused.endsAt !== undefined ? [
839
873
  {
840
- key: `${refused.key}:notice:at:${refused.endsAt}`,
874
+ key: `${key}:notice:at:${refused.endsAt}`,
841
875
  limit
842
876
  }
843
877
  ] : [
844
878
  bucket - 1,
845
879
  bucket
846
880
  ].map((at)=>({
847
- key: `${refused.key}:notice:${at}`,
881
+ key: `${key}:notice:${at}`,
848
882
  limit
849
883
  }));
850
884
  try {
@@ -1066,19 +1100,35 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1066
1100
  const guilds = options.config?.guilds ?? [];
1067
1101
  return !everythingTarget(options) && guilds.length > 0 && ids(guilds).length === 0;
1068
1102
  }
1103
+ /** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === discord_js.ApplicationCommandType.PrimaryEntryPoint;
1104
+ /** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
1069
1105
  /**
1070
1106
  * Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
1071
1107
  * there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
1108
+ *
1109
+ * A primary entry point command goes in the global update, as Discord refuses a guild update that holds one. A run
1110
+ * that sends everything to one guild sends nothing global, so it leaves the entry point out.
1072
1111
  */ function planTargets(commands, options, logger) {
1112
+ // Registering to the default scopes instead could publish everywhere
1113
+ if (options.onlyGuild !== undefined && !options.onlyGuild.trim()) {
1114
+ logger.error(BLANK_GUILD);
1115
+ return [];
1116
+ }
1073
1117
  const everythingTo = everythingTarget(options);
1074
- if (everythingTo) return [
1075
- {
1076
- scope: {
1077
- guild: everythingTo
1078
- },
1079
- commands
1118
+ if (everythingTo) {
1119
+ // A global update from here would hold only what this run sends, deleting the application's other global commands
1120
+ for (const command of commands.filter(isEntryPoint)){
1121
+ logger.warn(`Entry point command "${command.name}" is registered globally only; a production run or \`meocord register\` registers it.`);
1080
1122
  }
1081
- ];
1123
+ return [
1124
+ {
1125
+ scope: {
1126
+ guild: everythingTo
1127
+ },
1128
+ commands: commands.filter((command)=>!isEntryPoint(command))
1129
+ }
1130
+ ];
1131
+ }
1082
1132
  const defaultGuilds = ids(options.config?.guilds);
1083
1133
  const nowhere = defaultGuildsEmpty(options);
1084
1134
  const targets = new Map();
@@ -1096,7 +1146,12 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1096
1146
  });
1097
1147
  const unregistered = [];
1098
1148
  for (const command of commands){
1099
- if (command.guilds) {
1149
+ if (isEntryPoint(command)) {
1150
+ if (command.guilds) {
1151
+ logger.warn(`Entry point command "${command.name}" can only be registered globally, so the guilds its builder lists are ignored.`);
1152
+ }
1153
+ add('global', command);
1154
+ } else if (command.guilds) {
1100
1155
  const own = ids(command.guilds);
1101
1156
  // Falling back to the default scope could publish a staff-only command everywhere.
1102
1157
  if (own.length === 0) logger.warn(`Command "${command.name}" lists no guild ids in its builder, so it is not registered.`);
@@ -1114,7 +1169,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1114
1169
  }
1115
1170
  }
1116
1171
  if (nowhere) {
1117
- const outcome = unregistered.length === 0 ? 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
1172
+ const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
1118
1173
  logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
1119
1174
  }
1120
1175
  return [
@@ -1143,28 +1198,57 @@ const describeScope = (scope)=>scope === 'global' ? 'globally' : `to guild ${sco
1143
1198
  return path__default.default.join(nodeModules, '.cache', 'meocord', `commands-${applicationId}-${scopeKey(scope)}.json`);
1144
1199
  }
1145
1200
  const hashOf = (commands)=>crypto.createHash('sha256').update(JSON.stringify(commands.map(({ body })=>body))).digest('hex');
1146
- function readHash(file, logger) {
1201
+ function readRecord(file, logger) {
1147
1202
  if (!file || !fs.existsSync(file)) return undefined;
1148
1203
  try {
1149
- return JSON.parse(fs.readFileSync(file, 'utf8')).hash;
1204
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1150
1205
  } catch (error) {
1151
1206
  logger.debug(`Could not read the registered commands, so they are sent again: ${String(error)}`);
1152
1207
  return undefined;
1153
1208
  }
1154
1209
  }
1155
- function writeHash(file, hash, logger) {
1210
+ function writeRecord(file, record, logger) {
1156
1211
  if (!file) return;
1157
1212
  try {
1158
1213
  fs.mkdirSync(path__default.default.dirname(file), {
1159
1214
  recursive: true
1160
1215
  });
1161
- fs.writeFileSync(file, `${JSON.stringify({
1162
- hash
1163
- })}\n`);
1216
+ fs.writeFileSync(file, `${JSON.stringify(record)}\n`);
1164
1217
  } catch (error) {
1165
1218
  logger.debug(`Could not record the registered commands: ${String(error)}`);
1166
1219
  }
1167
1220
  }
1221
+ /** Forgets what development last sent to a scope, once that scope is written some other way. */ function forgetRecord(file, logger) {
1222
+ if (!file || !fs.existsSync(file)) return;
1223
+ try {
1224
+ fs.rmSync(file);
1225
+ } catch (error) {
1226
+ logger.debug(`Could not forget the registered commands: ${String(error)}`);
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Each command's `id:version`, sorted, from what Discord lists or answers a bulk update with; `undefined` for anything
1231
+ * else. Discord gives a command a new version whenever it is updated, so the set changes with any edit, removal or
1232
+ * re-registration, from this checkout or any other.
1233
+ */ function versionsOf(commands) {
1234
+ if (!Array.isArray(commands)) return undefined;
1235
+ const versions = commands.map((command)=>{
1236
+ const { id, version } = command ?? {};
1237
+ return typeof id === 'string' && typeof version === 'string' ? `${id}:${version}` : undefined;
1238
+ });
1239
+ return versions.every((version)=>version !== undefined) ? versions.sort() : undefined;
1240
+ }
1241
+ /** Whether Discord still holds a scope as development last sent it, by each command's version; a failed listing doesn't. */ async function stillAsSent(rest, route, record, scope, logger) {
1242
+ if (!record.versions) return false;
1243
+ let registered;
1244
+ try {
1245
+ registered = await rest.get(route);
1246
+ } catch (error) {
1247
+ logger.debug(`Could not list the commands registered ${describeScope(scope)}, so they are sent again: ${String(error)}`);
1248
+ return false;
1249
+ }
1250
+ return versionsOf(registered)?.join('\n') === record.versions.join('\n');
1251
+ }
1168
1252
  function logRegistered(target, logger) {
1169
1253
  const table = new Table__default.default({
1170
1254
  head: [
@@ -1187,22 +1271,29 @@ function logRegistered(target, logger) {
1187
1271
  };
1188
1272
  for (const { name, body } of target.commands){
1189
1273
  const options = Array.isArray(body.options) ? body.options : [];
1274
+ // A subcommand or a subcommand group, not a plain option
1275
+ const subcommands = options.filter((option)=>option.type === 1 || option.type === 2);
1190
1276
  table.push([
1191
1277
  name,
1192
1278
  typeNames[typeof body.type === 'number' ? body.type : 1] ?? 'Command',
1193
- options.map((option)=>option.name).join(', ')
1279
+ subcommands.map((option)=>option.name).join(', ')
1194
1280
  ]);
1195
1281
  }
1196
1282
  logger.log(`Registered ${target.commands.length} bot commands ${describeScope(target.scope)}:\n${table.toString()}`);
1197
1283
  }
1198
1284
  /**
1199
- * Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send, unless
1200
- * `force`. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
1285
+ * Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send and still
1286
+ * registered as sent, which one listing per skipped scope checks by each command's version, unless `force`. A blank `onlyGuild` registers nothing. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
1201
1287
  * leftovers, which `clearOther` removes; a development-guild run or a `commands.guilds` with no id only warns. Never
1202
1288
  * throws.
1203
1289
  * @returns Whether every scope was sent or unchanged, and `commands.guilds`, if set, lists a guild id.
1204
1290
  */ async function registerCommands(options) {
1205
1291
  const { rest, applicationId, controllerClasses, logger, config, development, onlyGuild, force = false } = options;
1292
+ // An unset variable passed as the guild; registering to the default scopes instead could publish everywhere
1293
+ if (onlyGuild !== undefined && !onlyGuild.trim()) {
1294
+ logger.error(BLANK_GUILD);
1295
+ return false;
1296
+ }
1206
1297
  const commands = collectCommands(controllerClasses, logger);
1207
1298
  if (!commands) return false;
1208
1299
  const targets = planTargets(commands, {
@@ -1219,18 +1310,26 @@ function logRegistered(target, logger) {
1219
1310
  let succeeded = !guildsEmpty;
1220
1311
  let sent = false;
1221
1312
  for (const target of targets){
1222
- const file = development ? cacheFile(applicationId, target.scope) : undefined;
1313
+ const file = cacheFile(applicationId, target.scope);
1223
1314
  const hash = hashOf(target.commands);
1224
- if (development && !force && readHash(file, logger) === hash) {
1315
+ const route = routeFor(applicationId, target.scope);
1316
+ // Unchanged since development last sent it, and still what Discord holds there
1317
+ const record = development && !force ? readRecord(file, logger) : undefined;
1318
+ if (record?.hash === hash && await stillAsSent(rest, route, record, target.scope, logger)) {
1225
1319
  logger.log(`Commands ${describeScope(target.scope)} are unchanged; not registering them again.`);
1226
1320
  continue;
1227
1321
  }
1228
1322
  try {
1229
- await rest.put(routeFor(applicationId, target.scope), {
1323
+ const answered = await rest.put(route, {
1230
1324
  body: target.commands.map(({ body })=>body)
1231
1325
  });
1232
1326
  sent = true;
1233
- if (development) writeHash(file, hash, logger);
1327
+ // Outside development the record would be stale, so the next development start sends again
1328
+ if (development) writeRecord(file, {
1329
+ hash,
1330
+ versions: versionsOf(answered)
1331
+ }, logger);
1332
+ else forgetRecord(file, logger);
1234
1333
  logRegistered(target, logger);
1235
1334
  } catch (error) {
1236
1335
  succeeded = false;
@@ -1259,13 +1358,17 @@ function logRegistered(target, logger) {
1259
1358
  if (!Array.isArray(existing) || existing.length === 0) continue;
1260
1359
  const names = existing.map(({ name })=>name).join(', ');
1261
1360
  if (!config?.clearOther || keptBecause) {
1262
- logger.warn(`${existing.length} command(s) are still registered ${describeScope(scope)} (${names}), which this ` + `configuration does not register to, so Discord keeps showing them there. ` + (config?.clearOther ? `clearOther is on, but they are not removed ${keptBecause}.` : 'Set commands.clearOther to remove them.'));
1361
+ // Advising clearOther when it would not apply sends the reader to a setting that changes nothing
1362
+ const advice = !keptBecause ? 'Set commands.clearOther to remove them.' : config?.clearOther ? `clearOther is on, but they are not removed ${keptBecause}.` : `They are not removed ${keptBecause}, even with commands.clearOther.`;
1363
+ logger.warn(`${existing.length} command(s) are still registered ${describeScope(scope)} (${names}), which this ` + `configuration does not register to, so Discord keeps showing them there. ${advice}`);
1263
1364
  continue;
1264
1365
  }
1265
1366
  try {
1266
1367
  await rest.put(routeFor(applicationId, scope), {
1267
1368
  body: []
1268
1369
  });
1370
+ // What development last sent there is gone, so its next start sends again
1371
+ forgetRecord(cacheFile(applicationId, scope), logger);
1269
1372
  logger.log(`Removed ${existing.length} command(s) left registered ${describeScope(scope)}: ${names}`);
1270
1373
  } catch (error) {
1271
1374
  logger.error(`Error removing the commands left registered ${describeScope(scope)}:`, error);
@@ -1376,7 +1479,7 @@ function isInterceptorWithParams(entry) {
1376
1479
  });
1377
1480
  /** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
1378
1481
  if (container.isBound(cls)) return;
1379
- if (injectedTokens(cls).includes(controller_decorator.ExecutionContext)) throw controller_decorator.refuse(singletonContextError(cls));
1482
+ if (injectedTokens(cls).includes(controller_decorator.ExecutionContext)) return controller_decorator.startupError(singletonContextError(cls));
1380
1483
  assertStartupChecked(container, cls);
1381
1484
  makeInjectable(cls);
1382
1485
  container.bind(cls).toSelf().inSingletonScope();
@@ -1571,7 +1674,7 @@ function isInterceptorWithParams(entry) {
1571
1674
  /** Binds a filter as a singleton, after checking it is one. */ function prepareFilter(container, entry) {
1572
1675
  const cls = filterClass(entry);
1573
1676
  if (!Reflect.hasOwnMetadata(controller_decorator.META.catchTypes, cls)) {
1574
- throw controller_decorator.refuse(new Error(`${cls.name || 'A filter'}: used as an exception filter, but not decorated with @Catch().`));
1677
+ return controller_decorator.startupError(new Error(`${cls.name || 'A filter'}: used as an exception filter, but not decorated with @Catch().`));
1575
1678
  }
1576
1679
  bindShared(container, cls);
1577
1680
  }
@@ -1683,11 +1786,12 @@ const asList = (entries)=>Array.isArray(entries) ? entries : [
1683
1786
 
1684
1787
  /** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
1685
1788
  const logger$1 = new controller_decorator.Logger('Observer');
1686
- /** Throws unless every entry is an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
1789
+ /** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
1687
1790
  for (const observer of observers){
1688
1791
  if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
1689
1792
  const name = typeof observer === 'function' ? observer.name : String(observer);
1690
- throw controller_decorator.refuse(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
1793
+ controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
1794
+ continue;
1691
1795
  }
1692
1796
  }
1693
1797
  }
@@ -1942,12 +2046,16 @@ const NO_GLOBAL_STAGES = {
1942
2046
  */ function bindAppPresenter(container, app, client) {
1943
2047
  const options = Reflect.getMetadata(controller_decorator.META.appOptions, app);
1944
2048
  if (!options?.presenter) return undefined;
1945
- bindShared(container, options.presenter);
2049
+ prepareAppPresenter(container, app);
1946
2050
  const presenter = container.get(options.presenter);
1947
2051
  container.bind(APP_PRESENTER).toConstantValue(presenter);
1948
2052
  if (client) controller_decorator.setPresenter(client, presenter);
1949
2053
  return presenter;
1950
2054
  }
2055
+ /** Binds the app's presenter class as a shared stage, checking it as one, without making it. */ function prepareAppPresenter(container, app) {
2056
+ const options = Reflect.getMetadata(controller_decorator.META.appOptions, app);
2057
+ if (options?.presenter) bindShared(container, options.presenter);
2058
+ }
1951
2059
  /** Where a container keeps the application's presenter. */ const APP_PRESENTER = Symbol('app_presenter');
1952
2060
  /** The presenter `bindAppPresenter` resolved into `container`, if any. */ function appPresenterOf(container) {
1953
2061
  return container.isBound(APP_PRESENTER) ? container.get(APP_PRESENTER) : undefined;
@@ -2037,18 +2145,21 @@ const stagesByGlobals = new WeakMap();
2037
2145
  /**
2038
2146
  * Binds the interceptors, filters and pipes every handler of `controllers` uses, and the global ones, as singletons, so
2039
2147
  * one that cannot be shared, or a filter without `@Catch`, fails at startup; refuses there `@Defer`, `@Validate`,
2040
- * `@UsePipe` and `@Cooldown` where they cannot apply, and two classes that keep state under one name.
2148
+ * `@UsePipe` and `@Cooldown` where they cannot apply, and two classes that keep state under one name. Binds only; it
2149
+ * makes nothing, so it runs with the other startup checks.
2041
2150
  */ function prepareHandlerStages(container, controllers) {
2042
2151
  assertDistinctNamesWhereKeyed(controllers);
2043
2152
  const globals = globalStagesOf(container);
2044
- configureThemes(container, globals.theme, controllers, globals.themeFor);
2045
2153
  for (const entry of globals.interceptors)prepareInterceptor(container, entry);
2046
2154
  for (const entry of globals.filters)prepareFilter(container, entry);
2047
2155
  for (const controller of controllers){
2048
2156
  const prototype = controller.prototype;
2049
2157
  for (const method of handlerMethods(prototype)){
2050
2158
  const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
2051
- if (kind) throw controller_decorator.refuse(deferMisuseError(controller.name, method, kind));
2159
+ if (kind) {
2160
+ controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
2161
+ continue;
2162
+ }
2052
2163
  for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
2053
2164
  for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
2054
2165
  for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
@@ -2056,6 +2167,10 @@ const stagesByGlobals = new WeakMap();
2056
2167
  assertInputStagesOnInteractions(controller, prototype);
2057
2168
  }
2058
2169
  }
2170
+ /** Records the app's theme for the handlers of `controllers`, once the startup checks have passed: it makes the theme cache. */ function prepareThemes(container, controllers) {
2171
+ const globals = globalStagesOf(container);
2172
+ configureThemes(container, globals.theme, controllers, globals.themeFor);
2173
+ }
2059
2174
  /** Whether a class has state keyed by its name: a cooldown on a handler, or a `@Once` handler. */ function keyedByName(cls) {
2060
2175
  const prototype = cls.prototype;
2061
2176
  const handlers = [
@@ -2072,7 +2187,8 @@ const stagesByGlobals = new WeakMap();
2072
2187
  for (const cls of classes){
2073
2188
  const other = byName.get(cls.name);
2074
2189
  if (other && other !== cls && (keyedByName(cls) || keyedByName(other))) {
2075
- throw controller_decorator.refuse(new Error(`${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share ` + `their counts. Rename one of them.`));
2190
+ controller_decorator.startupError(new Error(`${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share ` + `their counts. Rename one of them.`));
2191
+ continue;
2076
2192
  }
2077
2193
  byName.set(cls.name, cls);
2078
2194
  }
@@ -2100,11 +2216,12 @@ const stagesByGlobals = new WeakMap();
2100
2216
  const { schema, pipes } = handlerInputStages(prototype, method);
2101
2217
  const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
2102
2218
  if (schema || pipes.length > 0) {
2103
- throw controller_decorator.refuse(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
2219
+ controller_decorator.startupError(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
2220
+ continue;
2104
2221
  }
2105
2222
  // A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
2106
2223
  if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
2107
- throw controller_decorator.refuse(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
2224
+ controller_decorator.startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
2108
2225
  }
2109
2226
  }
2110
2227
  }
@@ -3261,7 +3378,8 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3261
3378
  if (own.hidden !== undefined && typeof own.hidden !== 'boolean') throw new Error(`hidden is true or false, not ${JSON.stringify(own.hidden)}.`);
3262
3379
  aliases = aliasPatterns(pattern, parsed.tokens, own.aliases);
3263
3380
  } catch (error) {
3264
- throw controller_decorator.refuse(new Error(`${controllerClass.name}.${method}: @MessageHandler('${pattern}'): ${error.message}`));
3381
+ controller_decorator.startupError(new Error(`${controllerClass.name}.${method}: @MessageHandler('${pattern}'): ${error.message}`));
3382
+ continue;
3265
3383
  }
3266
3384
  const shared = {
3267
3385
  controllerClass,
@@ -3300,7 +3418,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3300
3418
  routes.splice(j--, 1);
3301
3419
  continue;
3302
3420
  }
3303
- throw controller_decorator.refuse(new Error(`${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} ` + `match the same messages, so only one of them could ever answer those. Change one pattern, or give one its own prefix.`));
3421
+ controller_decorator.startupError(new Error(`${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} ` + `match the same messages, so only one of them could ever answer those. Change one pattern, or give one its own prefix.`));
3304
3422
  }
3305
3423
  }
3306
3424
  return routes;
@@ -4179,7 +4297,7 @@ const COMMAND_TYPES = new Set([
4179
4297
  *
4180
4298
  * @group Controllers
4181
4299
  * @see {@link HandlerEntry}
4182
- * @see {@link https://meocord.dev/docs/4.1/handler-discovery | Handler discovery}
4300
+ * @see {@link https://meocord.dev/docs/4.2/handler-discovery | Handler discovery}
4183
4301
  */ class HandlerRegistry {
4184
4302
  /**
4185
4303
  * Lists the registered handlers.
@@ -4424,7 +4542,7 @@ async function withTimeout(promise, ms, what) {
4424
4542
  *
4425
4543
  * @group Controllers
4426
4544
  * @see {@link ShardCallResult}
4427
- * @see {@link https://meocord.dev/docs/4.1/sharding | Sharding}
4545
+ * @see {@link https://meocord.dev/docs/4.2/sharding | Sharding}
4428
4546
  */ class ShardContext {
4429
4547
  /** The ids of the shards this process runs. With process sharding, one id; otherwise every shard, once ready. */ get ids() {
4430
4548
  if (this.client?.shard) return [
@@ -4601,9 +4719,9 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
4601
4719
  }
4602
4720
  const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
4603
4721
  /**
4604
- * Checks each provider's shape and indexes them by token, throwing on the first that cannot be bound, such as one with
4605
- * no token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a
4606
- * token provided twice.
4722
+ * Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
4723
+ * token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
4724
+ * provided twice, as a startup error; see `startupError`.
4607
4725
  */ function providerMap(providers, where) {
4608
4726
  const map = new Map();
4609
4727
  for (const provider of providers){
@@ -4612,10 +4730,12 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4612
4730
  if (typeof entry === 'function') {
4613
4731
  const name = tokenName(entry);
4614
4732
  const listing = where.includes('@MeoCord') ? 'list a service in @MeoCord({ services })' : 'a class a controller or service injects is bound for you, so it needs no listing';
4615
- throw controller_decorator.refuse(new Error(`${where}: ${name} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`));
4733
+ controller_decorator.startupError(new Error(`${where}: ${name} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`));
4734
+ continue;
4616
4735
  }
4617
4736
  if (!entry || typeof entry !== 'object' || !isToken(entry.provide)) {
4618
- throw controller_decorator.refuse(new Error(`${where}: a provider has no token: set provide to a class, a string or a symbol.`));
4737
+ controller_decorator.startupError(new Error(`${where}: a provider has no token: set provide to a class, a string or a symbol.`));
4738
+ continue;
4619
4739
  }
4620
4740
  const name = tokenName(entry.provide);
4621
4741
  const kinds = [
@@ -4624,26 +4744,35 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4624
4744
  'useFactory'
4625
4745
  ].filter((kind)=>kind in entry);
4626
4746
  if (kinds.length !== 1) {
4627
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
4747
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
4748
+ continue;
4628
4749
  }
4629
4750
  if ('useClass' in entry && typeof entry.useClass !== 'function') {
4630
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
4751
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
4752
+ continue;
4631
4753
  }
4632
4754
  if ('useClass' in entry && injectedTokens(entry.useClass).includes(controller_decorator.ExecutionContext)) {
4633
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} uses ${tokenName(entry.useClass)}, which injects ExecutionContext, but it is ` + "made once and shared, so it would keep the first call's context for every later call. Inject " + 'ExecutionContext only into guards.'));
4755
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} uses ${tokenName(entry.useClass)}, which injects ExecutionContext, but it is ` + "made once and shared, so it would keep the first call's context for every later call. Inject " + 'ExecutionContext only into guards.'));
4756
+ continue;
4634
4757
  }
4635
4758
  if ('useFactory' in entry) {
4636
4759
  if (typeof entry.useFactory !== 'function') {
4637
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} has a useFactory that is not a function.`));
4760
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useFactory that is not a function.`));
4761
+ continue;
4638
4762
  }
4639
4763
  if (entry.inject !== undefined && !(Array.isArray(entry.inject) && entry.inject.every(isToken))) {
4640
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} has an inject that is not a list of tokens.`));
4764
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} has an inject that is not a list of tokens.`));
4765
+ continue;
4641
4766
  }
4642
4767
  if (entry.inject?.includes(controller_decorator.ExecutionContext)) {
4643
- throw controller_decorator.refuse(new Error(`${where}: the provider for ${name} injects ExecutionContext, but its factory runs once and its value is ` + "shared, so it would keep the first call's context for every later call. Inject ExecutionContext only into guards."));
4768
+ controller_decorator.startupError(new Error(`${where}: the provider for ${name} injects ExecutionContext, but its factory runs once and its value is ` + "shared, so it would keep the first call's context for every later call. Inject ExecutionContext only into guards."));
4769
+ continue;
4644
4770
  }
4645
4771
  }
4646
- if (map.has(entry.provide)) throw controller_decorator.refuse(new Error(`${where}: ${name} is provided twice.`));
4772
+ if (map.has(entry.provide)) {
4773
+ controller_decorator.startupError(new Error(`${where}: ${name} is provided twice.`));
4774
+ continue;
4775
+ }
4647
4776
  map.set(entry.provide, provider);
4648
4777
  }
4649
4778
  return map;
@@ -4696,22 +4825,23 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4696
4825
  for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
4697
4826
  }
4698
4827
  /**
4699
- * Throws when a class or a factory asks for a string or symbol token that nothing binds, naming both,
4700
- * rather than failing when the class is first resolved. A class token is bound on demand, so it is
4701
- * never missing.
4828
+ * Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
4829
+ * when the class is first resolved. A class token is bound on demand, so it is never missing.
4702
4830
  */ function assertProvided(container, providers, classes, where) {
4703
4831
  const missing = (token)=>(typeof token === 'string' || typeof token === 'symbol') && !container.isBound(token);
4704
4832
  for (const cls of classes){
4705
4833
  const token = injectedTokens(cls).find(missing);
4706
4834
  if (token !== undefined) {
4707
- throw controller_decorator.refuse(new Error(`${cls.name}: it injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
4835
+ controller_decorator.startupError(new Error(`${cls.name}: it injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
4836
+ continue;
4708
4837
  }
4709
4838
  }
4710
4839
  for (const [provided, provider] of providers){
4711
4840
  const dependencies = isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [];
4712
4841
  const token = dependencies.find(missing);
4713
4842
  if (token !== undefined) {
4714
- throw controller_decorator.refuse(new Error(`${tokenName(provided)}: its provider injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
4843
+ controller_decorator.startupError(new Error(`${tokenName(provided)}: its provider injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
4844
+ continue;
4715
4845
  }
4716
4846
  }
4717
4847
  }
@@ -4744,16 +4874,21 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4744
4874
  */ function assertTypedParameters(classes, decorators) {
4745
4875
  for (const cls of classes){
4746
4876
  const instead = meocordClassAdvice(cls);
4747
- if (instead && undecoratedConstructor(cls)) throw controller_decorator.refuse(new Error(`${cls.name}: MeoCord does not inject it; ${instead}.`));
4877
+ if (instead && undecoratedConstructor(cls)) {
4878
+ controller_decorator.startupError(new Error(`${cls.name}: MeoCord does not inject it; ${instead}.`));
4879
+ continue;
4880
+ }
4748
4881
  if (undecoratedConstructor(cls)) {
4749
4882
  const decorator = decorators.get(cls);
4750
- throw controller_decorator.refuse(new Error(`${cls.name}: its constructor takes parameters, but ${cls.name} has no decorator, so TypeScript recorded none of ` + `their types and it cannot be created. ${decorator ? `Decorate it with ${decorator}.` : 'Decorate it with @Service(), or give a class from a package a provider in @MeoCord({ providers }).'}`));
4883
+ controller_decorator.startupError(new Error(`${cls.name}: its constructor takes parameters, but ${cls.name} has no decorator, so TypeScript recorded none of ` + `their types and it cannot be created. ${decorator ? `Decorate it with ${decorator}.` : 'Decorate it with @Service(), or give a class from a package a provider in @MeoCord({ providers }).'}`));
4884
+ continue;
4751
4885
  }
4752
4886
  const index = untypedParameter(cls);
4753
4887
  if (index === -1) continue;
4754
4888
  const injectors = classes.filter((other)=>other !== cls && injectedTokens(other).includes(cls)).map((other)=>other.name);
4755
4889
  const injectedBy = injectors.length === 0 ? '' : ` (${injectors.length === 1 ? injectors[0] : `${injectors.slice(0, -1).join(', ')} and ${injectors.at(-1)}`} ` + `inject${injectors.length === 1 ? 's' : ''} ${cls.name})`;
4756
- throw controller_decorator.refuse(new Error(`${cls.name}: parameter ${index + 1} of its constructor has no runtime type, so it cannot be created. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).'));
4890
+ controller_decorator.startupError(new Error(`${cls.name}: parameter ${index + 1} of its constructor has no runtime type, so it cannot be created. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).'));
4891
+ continue;
4757
4892
  }
4758
4893
  }
4759
4894
  /** For each container, the tokens MeoCord binds itself, each to the app's class it stands for, if any. */ const ownTokens = new WeakMap();
@@ -4822,15 +4957,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4822
4957
  }
4823
4958
  /**
4824
4959
  * Runs every factory in `order`, awaiting those that return a promise, so each value is made before
4825
- * anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
4826
- */ async function resolveProviders(container, providers, order) {
4960
+ * anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
4961
+ * it makes no further value once the app is stopping.
4962
+ */ async function resolveProviders(container, providers, order, control) {
4827
4963
  for (const token of order){
4828
4964
  const provider = providers.get(token);
4829
4965
  if (!provider || !isFactoryProvider(provider)) continue;
4966
+ if (control?.stopped()) return;
4967
+ control?.pending(tokenName(token));
4830
4968
  try {
4831
4969
  await container.getAsync(token);
4832
4970
  } catch (error) {
4833
4971
  throw providerFailure(token, error);
4972
+ } finally{
4973
+ control?.pending(undefined);
4834
4974
  }
4835
4975
  }
4836
4976
  }
@@ -4904,9 +5044,12 @@ exports.messageStartsFor = messageStartsFor;
4904
5044
  exports.needsGuild = needsGuild;
4905
5045
  exports.nonInteractionHandler = nonInteractionHandler;
4906
5046
  exports.observeUnclaimed = observeUnclaimed;
5047
+ exports.prepareAppPresenter = prepareAppPresenter;
4907
5048
  exports.prepareHandlerStages = prepareHandlerStages;
5049
+ exports.prepareThemes = prepareThemes;
4908
5050
  exports.providerMap = providerMap;
4909
5051
  exports.reachableClasses = reachableClasses;
5052
+ exports.recordInstanceContainers = recordInstanceContainers;
4910
5053
  exports.registerClientTheme = registerClientTheme;
4911
5054
  exports.registerCommands = registerCommands;
4912
5055
  exports.registrationKey = registrationKey;