meocord 4.1.2 → 4.2.0

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 +98 -0
  2. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
  3. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
  4. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
  5. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
  6. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
  7. package/dist/cjs/common/index.cjs +4 -4
  8. package/dist/cjs/core/index.cjs +195 -157
  9. package/dist/cjs/decorator/index.cjs +66 -57
  10. package/dist/cjs/testing/index.cjs +733 -301
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/README.md.template +3 -1
  13. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  15. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  17. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  19. package/dist/esm/build/load-config.pre-entry.js +4 -1
  20. package/dist/esm/common/decorator.js +6 -6
  21. package/dist/esm/common/deprecation.js +2 -2
  22. package/dist/esm/common/errors.js +2 -2
  23. package/dist/esm/common/execution-context.js +1 -1
  24. package/dist/esm/common/metadata.js +1 -1
  25. package/dist/esm/common/route.js +10 -1
  26. package/dist/esm/common/translate-error.js +1 -1
  27. package/dist/esm/core/command-conflicts.js +34 -34
  28. package/dist/esm/core/component-routes.js +74 -31
  29. package/dist/esm/core/dispatcher.js +51 -7
  30. package/dist/esm/core/filter-runner.js +2 -2
  31. package/dist/esm/core/handler-pipeline.js +27 -14
  32. package/dist/esm/core/handler-registry.js +1 -1
  33. package/dist/esm/core/interceptor-runner.js +2 -2
  34. package/dist/esm/core/meocord-factory.js +196 -157
  35. package/dist/esm/core/message-routes.js +4 -3
  36. package/dist/esm/core/observer-runner.js +4 -3
  37. package/dist/esm/core/providers.js +38 -21
  38. package/dist/esm/core/route-specificity.js +4 -4
  39. package/dist/esm/core/scalar-types.js +15 -3
  40. package/dist/esm/core/shard-context.js +1 -1
  41. package/dist/esm/core/startup-roots.js +2 -3
  42. package/dist/esm/core/theme-runtime.js +2 -2
  43. package/dist/esm/decorator/app.decorator.js +12 -7
  44. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  45. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  46. package/dist/esm/decorator/controller.decorator.js +110 -67
  47. package/dist/esm/decorator/cooldown.decorator.js +3 -3
  48. package/dist/esm/decorator/defer.decorator.js +4 -4
  49. package/dist/esm/decorator/event.decorator.js +5 -5
  50. package/dist/esm/decorator/filter.decorator.js +7 -7
  51. package/dist/esm/decorator/guard.decorator.js +8 -8
  52. package/dist/esm/decorator/inject.decorator.js +4 -4
  53. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  54. package/dist/esm/decorator/observer.decorator.js +4 -4
  55. package/dist/esm/decorator/service.decorator.js +4 -4
  56. package/dist/esm/decorator/theme.decorator.js +3 -3
  57. package/dist/esm/decorator/validation.decorator.js +10 -10
  58. package/dist/esm/package.json.js +1 -1
  59. package/dist/esm/testing/catalog.js +1 -1
  60. package/dist/esm/testing/cooldown-store-suite.js +18 -10
  61. package/dist/esm/testing/discord-defaults.js +60 -7
  62. package/dist/esm/testing/index.js +4 -2
  63. package/dist/esm/testing/inspect-handler.js +20 -3
  64. package/dist/esm/testing/meocord-testing-module.js +229 -188
  65. package/dist/esm/testing/mock-fn.js +80 -10
  66. package/dist/esm/testing/mock-interaction.js +223 -72
  67. package/dist/esm/testing/response.js +1 -1
  68. package/dist/esm/testing/routing.js +39 -14
  69. package/dist/esm/testing/startup-errors.js +26 -0
  70. package/dist/esm/testing/strict-mocks.js +45 -0
  71. package/dist/esm/testing/theme.js +1 -1
  72. package/dist/esm/util/bundle-entry.util.js +2 -1
  73. package/dist/esm/util/interaction.util.js +1 -1
  74. package/dist/esm/util/meocord-config-validation.util.js +1 -0
  75. package/dist/esm/util/metadata-keys.js +1 -0
  76. package/dist/esm/util/refusal.util.js +148 -8
  77. package/dist/types/common/index.d.cts +8 -8
  78. package/dist/types/common/index.d.ts +8 -8
  79. package/dist/types/core/index.d.cts +8 -6
  80. package/dist/types/core/index.d.ts +8 -6
  81. package/dist/types/decorator/index.d.cts +30 -29
  82. package/dist/types/decorator/index.d.ts +30 -29
  83. package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
  84. package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
  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-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
  88. package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
  89. package/dist/types/testing/index.d.cts +245 -31
  90. package/dist/types/testing/index.d.ts +245 -31
  91. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
  92. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
  93. package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
  94. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
  95. package/package.json +3 -2
@@ -34,18 +34,30 @@ const UNIT_MS = lookupTable({
34
34
  return total + Number(amount) * UNIT_MS[unit];
35
35
  }, 0);
36
36
  }
37
+ /** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
38
+ /**
39
+ * A Discord ID, kept as its text: 17 to 20 digits, no leading zero, up to the largest 64-bit value. An ID's top 42 bits
40
+ * are milliseconds since 2015-01-01, so every ID made from 2015-01-28 on has 17 digits or more, and none is a safe
41
+ * integer.
42
+ */ const snowflake = (word)=>/^[1-9]\d{16,19}$/.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
43
+ /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(word) ? word : undefined;
37
44
  /** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
38
45
  /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
39
46
  string: (segment)=>segment,
40
47
  int: (segment)=>number(segment, true),
41
48
  number: (segment)=>number(segment, false),
42
- bool
49
+ bool,
50
+ snowflake,
51
+ uuid
43
52
  });
44
- /** Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, or words to choose from. */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
53
+ /**
54
+ * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
55
+ * choose from.
56
+ */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
45
57
  /** A customId segment as its type's value, words to choose from compared as written: `undefined` when it is not one. */ function parseSegment(type, segment) {
46
58
  const choices = choicesOf(type);
47
59
  if (choices) return choices.includes(segment) ? segment : undefined;
48
60
  return SEGMENT_TYPES[type](segment);
49
61
  }
50
62
 
51
- export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment };
63
+ export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, snowflake, uuid };
@@ -95,7 +95,7 @@ async function withTimeout(promise, ms, what) {
95
95
  *
96
96
  * @group Controllers
97
97
  * @see {@link ShardCallResult}
98
- * @see {@link https://meocord.dev/docs/4.1/sharding | Sharding}
98
+ * @see {@link https://meocord.dev/docs/4.2/sharding | Sharding}
99
99
  */ class ShardContext {
100
100
  /** The ids of the shards this process runs. With process sharding, one id; otherwise every shard, once ready. */ get ids() {
101
101
  if (this.client?.shard) return [
@@ -4,7 +4,7 @@ import { classDecorators, handlerStageClasses } from './handler-pipeline.js';
4
4
  import { reachableClasses, assertTypedParameters } from './providers.js';
5
5
  import { themeResolverClass } from './theme-resolvers.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
- import { refuse } from '../util/refusal.util.js';
7
+ import { startupError } from '../util/refusal.util.js';
8
8
 
9
9
  /** The guards `@UseGuard` puts on a class's methods, handlers or not, which a direct call runs too. */ function methodGuards(cls) {
10
10
  const guards = [];
@@ -75,8 +75,7 @@ import { refuse } from '../util/refusal.util.js';
75
75
  /** Runs the startup checks over every class an app runs, so a class that cannot be made is refused by name now. */ function assertStartupClasses({ classes, decorators }, { translator }) {
76
76
  assertTypedParameters(classes, decorators);
77
77
  if (translator) return;
78
- const needsOne = classes.find((cls)=>injectedTokens(cls).includes(Translator));
79
- if (needsOne) throw refuse(missingTranslatorError(needsOne));
78
+ for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
80
79
  }
81
80
 
82
81
  export { assertStartupClasses, lateGuardCheck, startupClasses };
@@ -1,8 +1,8 @@
1
1
  import 'reflect-metadata';
2
2
  import { stageClasses, sourcePrototype } from './guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
- import { resolverCaches, ThemeCache, attachThemeCache, lookupLayers } from './theme-resolvers.js';
5
- import { releaseAmbientTheme, claimAmbientTheme, defaultTheme, mergeTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, hasAmbientTheme, setThemeOutsideCalls, useTheme } from './theme-scope.js';
4
+ import { lookupLayers, resolverCaches, ThemeCache, attachThemeCache } from './theme-resolvers.js';
5
+ import { releaseAmbientTheme, claimAmbientTheme, mergeTheme, defaultTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, hasAmbientTheme, setThemeOutsideCalls, useTheme } from './theme-scope.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
7
 
8
8
  /** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
@@ -7,7 +7,7 @@ import { BUILT_IN_TYPES } from '../core/message-params.js';
7
7
  import { assertObservers } from '../core/observer-runner.js';
8
8
  import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
9
9
  import { copyLayer } from '../core/theme-scope.js';
10
- import { deprecatedOnMethod, refuse } from '../util/refusal.util.js';
10
+ import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
11
11
  import { timeoutProblem } from '../util/timer-limit.util.js';
12
12
  import { META } from '../util/metadata-keys.js';
13
13
 
@@ -18,14 +18,19 @@ import { META } from '../util/metadata-keys.js';
18
18
  if (prefix !== undefined && !isText(prefix) && typeof prefix !== 'function' && !(Array.isArray(prefix) && prefix.every(isText))) {
19
19
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { prefix } }) takes a string, a list of strings, or a function of the message returning them.`));
20
20
  }
21
- for (const [name, value] of Object.entries({
21
+ const flags = {
22
22
  caseSensitive,
23
23
  replyEmoji: messages.replyEmoji,
24
24
  dmOnError: messages.dmOnError,
25
- dmOnCooldown: messages.dmOnCooldown
26
- })){
25
+ dmOnCooldown: messages.dmOnCooldown,
26
+ slowHandlerWarning: messages.slowHandlerWarning
27
+ };
28
+ for (const [name, value] of Object.entries(flags)){
27
29
  if (value !== undefined && typeof value !== 'boolean') throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { ${name} } }) takes true or false.`));
28
30
  }
31
+ if (messages.handlers !== undefined && messages.handlers !== 'sequential' && messages.handlers !== 'concurrent') {
32
+ throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { handlers } }) takes 'sequential' or 'concurrent'.`));
33
+ }
29
34
  if (mention !== undefined && typeof mention !== 'boolean' && mention !== 'only') {
30
35
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { mention } }) takes true, false or 'only'.`));
31
36
  }
@@ -87,9 +92,9 @@ import { META } from '../util/metadata-keys.js';
87
92
  * @see {@link MeoCordOptions}
88
93
  * @see {@link MeoCordFactory}
89
94
  * @see {@link MeoCordConfig}
90
- * @see {@link https://meocord.dev/docs/4.1/configuration | Configuration}
95
+ * @see {@link https://meocord.dev/docs/4.2/configuration | Configuration}
91
96
  */ function MeoCord(options) {
92
- return (target, propertyKey)=>{
97
+ return declaring((target, propertyKey)=>{
93
98
  if (deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
94
99
  assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
95
100
  assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
@@ -111,7 +116,7 @@ import { META } from '../util/metadata-keys.js';
111
116
  ...options,
112
117
  theme
113
118
  }, target);
114
- };
119
+ });
115
120
  }
116
121
  /** Refuses theme resolvers and cache options the runtime cannot follow, where the app is declared. */ function assertThemeFor(appName, { themeFor, themeCache, themeForTimeoutMs }) {
117
122
  const problem = themeFor === undefined ? undefined : themeForProblem(themeFor, `${appName}: @MeoCord({ themeFor })`, 'or a class implementing ThemeResolver.');
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
- import { deprecatedOnMethod } from '../util/refusal.util.js';
4
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
5
 
6
6
  /**
7
7
  * Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
@@ -30,9 +30,9 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
30
30
  * @group Decorators
31
31
  * @category Handlers
32
32
  * @see {@link Command}
33
- * @see {@link https://meocord.dev/docs/4.1/slash-commands | Slash commands}
33
+ * @see {@link https://meocord.dev/docs/4.2/slash-commands | Slash commands}
34
34
  */ function CommandBuilder(commandType, options = {}) {
35
- return function(target, propertyKey) {
35
+ return declaring(function(target, propertyKey) {
36
36
  if (deprecatedOnMethod('@CommandBuilder', target, propertyKey)) return;
37
37
  makeInjectable(target);
38
38
  // Define the command type metadata for the target class
@@ -40,7 +40,7 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
40
40
  if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
41
41
  ...options.guilds
42
42
  ], target);
43
- };
43
+ });
44
44
  }
45
45
 
46
46
  export { CommandBuilder };
@@ -1,7 +1,8 @@
1
+ import { dropInheritedRoutes } from './controller.decorator.js';
1
2
  import { guardOwnHandlersWithBaseGuards } from './guard.decorator.js';
2
3
  import { makeInjectable } from '../util/injectable.util.js';
3
4
  import { META } from '../util/metadata-keys.js';
4
- import { deprecatedOnMethod } from '../util/refusal.util.js';
5
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
6
 
6
7
  /**
7
8
  * Marks a class as a controller, whose methods handle commands, components, messages, reactions or events.
@@ -35,14 +36,18 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
35
36
  * @category Controllers
36
37
  * @see {@link Command}
37
38
  * @see {@link ControllerOptions}
38
- * @see {@link https://meocord.dev/docs/4.1/how-a-call-runs | How a call runs}
39
+ * @see {@link https://meocord.dev/docs/4.2/how-a-call-runs | How a call runs}
39
40
  */ function Controller(options = {}) {
40
- return function(target, propertyKey) {
41
+ return declaring(function(target, propertyKey) {
41
42
  if (deprecatedOnMethod('@Controller', target, propertyKey)) return;
42
43
  makeInjectable(target);
43
44
  if (options.inheritStages === false) Reflect.defineMetadata(META.inheritStages, false, target);
45
+ if (options.inheritedRoutes === 'replace') {
46
+ dropInheritedRoutes(target.prototype);
47
+ Reflect.defineMetadata(META.replacesInheritedRoutes, true, target.prototype);
48
+ }
44
49
  guardOwnHandlersWithBaseGuards(target);
45
- };
50
+ });
46
51
  }
47
52
 
48
53
  export { Controller };
@@ -6,8 +6,8 @@ import { matchesCommandType, interactionClassName, isCustomIdRouted } from '../u
6
6
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
7
7
  import { Logger } from '../common/logger.js';
8
8
  import { routeSpecificity } from '../core/route-specificity.js';
9
- import { isSegmentType, choicesOf, lookupTable, parseSegment } from '../core/scalar-types.js';
10
- import { refuseOnClass, refuse } from '../util/refusal.util.js';
9
+ import { lookupTable, isSegmentType, choicesOf, parseSegment } from '../core/scalar-types.js';
10
+ import { declaring, refuse, refuseOnClass } from '../util/refusal.util.js';
11
11
  import { withArticle, describeValue } from '../util/value.util.js';
12
12
  import { META } from '../util/metadata-keys.js';
13
13
 
@@ -69,6 +69,42 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
69
69
  else handlers[index] = entry;
70
70
  Reflect.defineMetadata(key, handlers, target);
71
71
  }
72
+ /**
73
+ * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
74
+ * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
75
+ */ function dropInheritedRoutes(prototype) {
76
+ const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
77
+ if (redeclared.size === 0) return;
78
+ const base = Object.getPrototypeOf(prototype);
79
+ // An entry the class takes from its base, for a method it re-declares
80
+ const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
81
+ const commands = ownCommandMap(prototype);
82
+ const baseCommands = Reflect.getMetadata(META.commands, base) ?? {};
83
+ for (const [name, metas] of Object.entries(commands)){
84
+ const kept = metas.filter((meta)=>!inheritedFor(baseCommands[name] ?? [], (entry)=>entry.methodName)(meta));
85
+ if (kept.length > 0) commands[name] = kept;
86
+ else delete commands[name];
87
+ }
88
+ Reflect.defineMetadata(META.commands, commands, prototype);
89
+ const lists = [
90
+ [
91
+ META.messageHandlers,
92
+ (entry)=>entry.method
93
+ ],
94
+ [
95
+ META.reactionHandlers,
96
+ (entry)=>entry.method
97
+ ],
98
+ [
99
+ META.autocompleteHandlers,
100
+ (entry)=>entry.methodName
101
+ ]
102
+ ];
103
+ for (const [key, method] of lists){
104
+ const inherited = Reflect.getMetadata(key, base) ?? [];
105
+ Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
106
+ }
107
+ }
72
108
  /** The class's own command map, started from a copy of the inherited one, for the same reason. */ function ownCommandMap(target) {
73
109
  const own = Reflect.getOwnMetadata(META.commands, target);
74
110
  if (own) return own;
@@ -81,7 +117,7 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
81
117
  ]));
82
118
  }
83
119
  function MessageHandler(pattern, options = {}) {
84
- return function(target, propertyKey) {
120
+ return declaring(function(target, propertyKey) {
85
121
  refuseOnClass('@MessageHandler', target, propertyKey);
86
122
  // An empty pattern means every message, as no pattern does
87
123
  if (pattern === '') {
@@ -95,7 +131,7 @@ function MessageHandler(pattern, options = {}) {
95
131
  };
96
132
  addOwnHandler(META.messageHandlers, target, declared, (other)=>other.method === method && other.pattern === declared.pattern);
97
133
  declareRoute(target, messageRoute(method, declared.pattern));
98
- };
134
+ });
99
135
  }
100
136
  function ReactionHandler(emojiOrSettings, settings = {}) {
101
137
  const [emoji, own] = typeof emojiOrSettings === 'object' ? [
@@ -105,7 +141,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
105
141
  emojiOrSettings,
106
142
  settings
107
143
  ];
108
- return function(target, propertyKey) {
144
+ return declaring(function(target, propertyKey) {
109
145
  refuseOnClass('@ReactionHandler', target, propertyKey);
110
146
  const method = propertyKey.toString();
111
147
  addOwnHandler(META.reactionHandlers, target, {
@@ -114,7 +150,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
114
150
  settings: own
115
151
  }, (other)=>other.method === method && other.emoji === emoji);
116
152
  declareRoute(target, reactionRoute(method, emoji));
117
- };
153
+ });
118
154
  }
119
155
  /** A custom emoji as Discord writes it in a message: `<:name:id>`, or `<a:name:id>` for an animated one. */ const CUSTOM_EMOJI = /^<a?:\w+:(\d+)>$/;
120
156
  /**
@@ -146,7 +182,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
146
182
  /** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
147
183
  /** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
148
184
  /**
149
- * Compiles a pattern into a regex, its parameter names, their types and its specificity. A `{name}` takes a
185
+ * Compiles a pattern into a regex, its parameter names, their types and its 4.1 specificity. A `{name}` takes a
150
186
  * whole `/`-separated segment, so a uuid is captured whole and `profile/{uuid}` never overlaps
151
187
  * `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
152
188
  */ function createRegexFromPattern(pattern) {
@@ -186,14 +222,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
186
222
  literalLength += trailing.length;
187
223
  regexPattern += escapeLiteral(trailing);
188
224
  const regex = new RegExp(`^${regexPattern}$`);
189
- // Literal text is the signal: a pattern spelling out more of the id describes it
190
- // more exactly than one leaving it to a parameter. Fewer parameters breaks a tie
191
- // between equal-length patterns, then narrower typed params; the order the controllers
192
- // and handlers are listed in settles what is left, and the startup warning names the pair.
225
+ // What 4.1 ranked by, kept for code that reads CommandMeta.specificity; routing ranks segment by segment
193
226
  const specificity = routeSpecificity({
194
227
  literals: literalLength,
195
228
  params: params.length,
196
- typed: Object.values(types).reduce((sum, type)=>sum + narrowness(type), 0)
229
+ typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
197
230
  });
198
231
  return {
199
232
  regex,
@@ -202,9 +235,8 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
202
235
  specificity
203
236
  };
204
237
  }
205
- /** How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, int, number. */ const narrowness = (type)=>choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1;
206
238
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
207
- const kinds = 'string, int, number, bool, or words to choose from such as {mode:on|off}';
239
+ const kinds = 'string, int, number, bool, snowflake, uuid, or words to choose from such as {mode:on|off}';
208
240
  if ([
209
241
  'member',
210
242
  'user',
@@ -228,10 +260,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
228
260
  * the keys the handler's params require are checked against the pattern or route, a typed segment's value against
229
261
  * its type, and a select menu's choices, such as `values: string[]`, against what discord.js gives; a key a pipe
230
262
  * produces is declared `Piped<T>`, and left to the pipe. Two component handlers of one type whose patterns match
231
- * exactly the same ids stop the bot at startup; patterns that only overlap are warned about, naming the one that
232
- * runs: the more specific, or between equally specific ones, the one listed first. In the next major version (5.0),
233
- * the one that spells out the first segment where two equally specific patterns differ runs instead, and the warning
234
- * names the pairs that changes. A context menu handler receives the kind its builder's `setType()` names, and one
263
+ * exactly the same ids stop the bot at startup. Patterns that both match an id rank segment by segment, left to
264
+ * right: at the first segment one spells out as literal text and the other leaves to a param, the literal one runs,
265
+ * so `profile/me/{section}` takes `profile/me/edit` from `profile/{userId}/edit`; between patterns that leaves tied,
266
+ * the narrower type at the first param where they differ. Two still tied are warned about as the bot starts, and the
267
+ * one listed first runs. A context menu handler receives the kind its builder's `setType()` names, and one
235
268
  * declaring the other kind fails to compile; when the compiler cannot tell the kind, `@Command` checks the parameter
236
269
  * type it emits as it applies. A subclass that re-declares an inherited handler on the same name or pattern takes its
237
270
  * own builder and options; on another it still answers the inherited one too, which the bot warns about as it
@@ -264,11 +297,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
264
297
  * @category Handlers
265
298
  * @see {@link CommandBuilder}
266
299
  * @see {@link route}
267
- * @see {@link https://meocord.dev/docs/4.1/slash-commands | Slash commands}
268
- * @see {@link https://meocord.dev/docs/4.1/components | Buttons, selects and modals}
300
+ * @see {@link https://meocord.dev/docs/4.2/slash-commands | Slash commands}
301
+ * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
269
302
  */ function Command(name, builderOrType) {
270
303
  const commandName = typeof name === 'string' ? name : name.pattern;
271
- return function(target, propertyKey, _descriptor) {
304
+ return declaring(function(target, propertyKey, _descriptor) {
272
305
  var _commands, _commandName;
273
306
  refuseOnClass('@Command', target, propertyKey);
274
307
  const originalMethod = _descriptor.value;
@@ -349,10 +382,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
349
382
  cause: error
350
383
  }));
351
384
  }
352
- const { regex: generatedRegex, params, specificity: patternSpecificity } = pattern;
353
- regex = generatedRegex;
354
- dynamicParams = params;
355
- specificity = patternSpecificity;
385
+ regex = pattern.regex;
386
+ dynamicParams = pattern.params;
387
+ specificity = pattern.specificity;
356
388
  }
357
389
  const declared = {
358
390
  methodName: propertyKey,
@@ -376,7 +408,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
376
408
  else metas[replaced] = declared;
377
409
  Reflect.defineMetadata(META.commands, commands, target);
378
410
  declareRoute(target, commandRoute(propertyKey, commandType, commandName));
379
- };
411
+ });
380
412
  }
381
413
  /** The interaction class Discord sends for each context menu kind. */ const CONTEXT_MENU_INTERACTIONS = new Map([
382
414
  [
@@ -448,9 +480,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
448
480
  * @group Decorators
449
481
  * @category Handlers
450
482
  * @see {@link Command}
451
- * @see {@link https://meocord.dev/docs/4.1/autocomplete | Autocomplete}
483
+ * @see {@link https://meocord.dev/docs/4.2/autocomplete | Autocomplete}
452
484
  */ function Autocomplete(commandPath, optionName) {
453
- return function(target, propertyKey, _descriptor) {
485
+ return declaring(function(target, propertyKey, _descriptor) {
454
486
  refuseOnClass('@Autocomplete', target, propertyKey);
455
487
  const methodName = propertyKey.toString();
456
488
  addOwnHandler(META.autocompleteHandlers, target, {
@@ -459,7 +491,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
459
491
  methodName
460
492
  }, (other)=>other.methodName === methodName && other.commandPath === commandPath && other.optionName === optionName);
461
493
  declareRoute(target, autocompleteRoute(methodName, commandPath, optionName));
462
- };
494
+ });
463
495
  }
464
496
  /**
465
497
  * Returns a controller's autocomplete handlers, option-specific ones first.
@@ -481,52 +513,63 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
481
513
  const match = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`).exec(segment);
482
514
  return match ? typeKey(match[2]) : undefined;
483
515
  }
484
- /** Whether a segment's two readings can both take one value: a literal, text, or a typed param. */ function segmentsOverlap(left, right) {
516
+ /**
517
+ * How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, uuid,
518
+ * snowflake, int, number, and text last. A snowflake is narrower than a number, which takes all its values.
519
+ */ const NARROWNESS = lookupTable({
520
+ bool: 5,
521
+ uuid: 4,
522
+ snowflake: 3,
523
+ int: 2,
524
+ number: 1,
525
+ '': 0
526
+ });
527
+ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
528
+ /**
529
+ * How narrow a pattern segment that is a param, `{name}` or `{name:type}`, is: from `0` for text to `4` for words to
530
+ * choose from. `undefined` for a segment of literal text.
531
+ */ function paramNarrowness(segment) {
532
+ const type = segmentParamType(segment);
533
+ return type === undefined ? undefined : narrowness(type);
534
+ }
535
+ /** A value of each scalar segment type: an int, a bool, a snowflake and a uuid; text takes any. */ const SAMPLE_VALUES = [
536
+ '1',
537
+ 'true',
538
+ '10000000000000000',
539
+ '00000000-0000-0000-0000-000000000000'
540
+ ];
541
+ /** A value both readings of a segment take: a literal, text, or a typed param; `undefined` when they share none. */ function sharedValue(left, right) {
485
542
  const [a, b] = [
486
543
  segmentParamType(left),
487
544
  segmentParamType(right)
488
545
  ];
489
- if (a === undefined && b === undefined) return left === right;
490
- if (a === undefined || b === undefined) {
491
- const [type, literal] = a === undefined ? [
492
- b,
546
+ const takes = (type, segment, value)=>type === undefined ? segment === value : type === '' || parseSegment(type, value) !== undefined;
547
+ // Every type takes one of these when it takes anything another reading takes: a literal, a listed word, or a value
548
+ // of each scalar type
549
+ const candidates = [
550
+ ...a === undefined ? [
493
551
  left
494
- ] : [
495
- a,
552
+ ] : [],
553
+ ...b === undefined ? [
496
554
  right
497
- ];
498
- return type === '' || parseSegment(type, literal) !== undefined;
499
- }
500
- if (a === '' || b === '' || a === b) return true;
501
- // Words to choose from overlap another type when one of them is a value of it
502
- const [wordsA, wordsB] = [
503
- choicesOf(a),
504
- choicesOf(b)
555
+ ] : [],
556
+ ...choicesOf(a ?? '') ?? [],
557
+ ...choicesOf(b ?? '') ?? [],
558
+ ...SAMPLE_VALUES
505
559
  ];
506
- if (wordsA) return wordsA.some((word)=>parseSegment(b, word) !== undefined);
507
- if (wordsB) return wordsB.some((word)=>parseSegment(a, word) !== undefined);
508
- // Of the scalar types, only a whole number and a number share values
509
- return a === 'int' && b === 'number' || a === 'number' && b === 'int';
560
+ return candidates.find((value)=>takes(a, left, value) && takes(b, right, value));
510
561
  }
511
562
  /**
512
- * Finds pairs of customId patterns that can both match one id, such as `a/{x}/c` and `a/b/{y}`.
513
- * @returns Each ambiguous pair once, in the order the patterns were given.
514
- */ function findAmbiguousRoutes(patterns) {
515
- const segmentsOf = (pattern)=>pattern.split(PARAM_SEPARATOR);
516
- const collisions = [];
517
- for(let i = 0; i < patterns.length; i++){
518
- for(let j = i + 1; j < patterns.length; j++){
519
- const left = segmentsOf(patterns[i]);
520
- const right = segmentsOf(patterns[j]);
521
- if (left.length !== right.length) continue;
522
- const disjoint = left.some((segment, index)=>!segmentsOverlap(segment, right[index]));
523
- if (!disjoint) collisions.push([
524
- patterns[i],
525
- patterns[j]
526
- ]);
527
- }
528
- }
529
- return collisions;
563
+ * A customId both patterns match, such as `ticket/1/close` for `ticket/{id}/close` and `ticket/{n:int}/close`;
564
+ * `undefined` when no id matches both.
565
+ */ function sharedCustomId(left, right) {
566
+ const [a, b] = [
567
+ left.split(PARAM_SEPARATOR),
568
+ right.split(PARAM_SEPARATOR)
569
+ ];
570
+ if (a.length !== b.length) return undefined;
571
+ const values = a.map((segment, index)=>sharedValue(segment, b[index]));
572
+ return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
530
573
  }
531
574
 
532
- export { Autocomplete, Command, MessageHandler, PARAM_SEPARATOR, ReactionHandler, createRegexFromPattern, findAmbiguousRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, matchesEmoji, ownHandlerList, patternShape };
575
+ export { Autocomplete, Command, MessageHandler, PARAM_SEPARATOR, ReactionHandler, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import { refuse, decoratedName } from '../util/refusal.util.js';
2
+ import { declaring, refuse, decoratedName } from '../util/refusal.util.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
4
 
5
5
  function Cooldown(options) {
@@ -10,7 +10,7 @@ function Cooldown(options) {
10
10
  per,
11
11
  windowMs: Math.round(seconds * 1000)
12
12
  };
13
- return function(target, propertyKey) {
13
+ return declaring(function(target, propertyKey) {
14
14
  // Checked where it applies, so the refusal names the handler or controller
15
15
  const problem = cooldownProblem(seconds, uses, per, by);
16
16
  if (problem) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${problem}`));
@@ -28,7 +28,7 @@ function Cooldown(options) {
28
28
  ...existing
29
29
  ], target, propertyKey);
30
30
  }
31
- };
31
+ });
32
32
  }
33
33
  /**
34
34
  * The longest window, in seconds: half the furthest time a `Date` holds, 8.64e15 ms, so the moment a wait ends, now
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { nonInteractionHandler, deferMisuseError } from '../core/defer.js';
3
- import { refuseOnClass, refuse } from '../util/refusal.util.js';
3
+ import { declaring, refuseOnClass, refuse } from '../util/refusal.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
 
6
6
  /**
@@ -37,9 +37,9 @@ import { META } from '../util/metadata-keys.js';
37
37
  * @category Pipeline stages
38
38
  * @see {@link DeferOptions}
39
39
  * @see {@link respond}
40
- * @see {@link https://meocord.dev/docs/4.1/defer | Deferring}
40
+ * @see {@link https://meocord.dev/docs/4.2/defer | Deferring}
41
41
  */ function Defer(options = {}) {
42
- return (target, propertyKey)=>{
42
+ return declaring((target, propertyKey)=>{
43
43
  refuseOnClass('@Defer', target, propertyKey);
44
44
  const methodName = String(propertyKey);
45
45
  const kind = nonInteractionHandler(target, methodName);
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
47
47
  Reflect.defineMetadata(META.deferOptions, {
48
48
  ...options
49
49
  }, target, methodName);
50
- };
50
+ });
51
51
  }
52
52
 
53
53
  export { Defer };
@@ -1,11 +1,11 @@
1
1
  import 'reflect-metadata';
2
2
  import { ownHandlerList } from './controller.decorator.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
- import { refuseOnClass } from '../util/refusal.util.js';
4
+ import { declaring, refuseOnClass } from '../util/refusal.util.js';
5
5
 
6
6
  function eventDecorator(event, once) {
7
7
  // Generic over the method, so it is checked against the event's arguments rather than required to match exactly
8
- return function(target, propertyKey, _descriptor) {
8
+ return declaring(function(target, propertyKey, _descriptor) {
9
9
  refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
10
10
  const handlers = ownHandlerList(META.eventHandlers, target);
11
11
  handlers.push({
@@ -14,7 +14,7 @@ function eventDecorator(event, once) {
14
14
  once
15
15
  });
16
16
  Reflect.defineMetadata(META.eventHandlers, handlers, target);
17
- };
17
+ });
18
18
  }
19
19
  /**
20
20
  * Handles a discord.js client event every time it is emitted, on a controller or a service.
@@ -42,7 +42,7 @@ function eventDecorator(event, once) {
42
42
  * @group Decorators
43
43
  * @category Handlers
44
44
  * @see {@link Once}
45
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
45
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
46
46
  */ function On(event) {
47
47
  return eventDecorator(event, false);
48
48
  }
@@ -69,7 +69,7 @@ function eventDecorator(event, once) {
69
69
  * @group Decorators
70
70
  * @category Handlers
71
71
  * @see {@link On}
72
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
72
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
73
73
  */ function Once(event) {
74
74
  return eventDecorator(event, true);
75
75
  }
@@ -5,7 +5,7 @@ import { Logger } from '../common/logger.js';
5
5
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
6
6
  import { isConstructor, describeValue } from '../util/value.util.js';
7
7
  import { META } from '../util/metadata-keys.js';
8
- import { refuseOnMethod } from '../util/refusal.util.js';
8
+ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
9
9
 
10
10
  /**
11
11
  * Marks a class as an exception filter for the given error types.
@@ -34,9 +34,9 @@ import { refuseOnMethod } from '../util/refusal.util.js';
34
34
  * @group Decorators
35
35
  * @category Pipeline stages
36
36
  * @see {@link UseFilter}
37
- * @see {@link https://meocord.dev/docs/4.1/exception-filters | Exception filters}
37
+ * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
38
38
  */ function Catch(...errorTypes) {
39
- return function(target, propertyKey) {
39
+ return declaring(function(target, propertyKey) {
40
40
  refuseOnMethod('@Catch', target, propertyKey);
41
41
  makeInjectable(target);
42
42
  errorTypes.forEach((type, index)=>{
@@ -45,7 +45,7 @@ import { refuseOnMethod } from '../util/refusal.util.js';
45
45
  warnDeprecatedBehaviour(logger, `${target.name}: @Catch's ${entry}, ${describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
46
46
  });
47
47
  Reflect.defineMetadata(META.catchTypes, errorTypes, target);
48
- };
48
+ });
49
49
  }
50
50
  const logger = new Logger('Catch');
51
51
  const ORDINALS = [
@@ -85,9 +85,9 @@ const ORDINALS = [
85
85
  * @group Decorators
86
86
  * @category Pipeline stages
87
87
  * @see {@link Catch}
88
- * @see {@link https://meocord.dev/docs/4.1/exception-filters | Exception filters}
88
+ * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
89
89
  */ function UseFilter(...filters) {
90
- return function(target, propertyKey) {
90
+ return declaring(function(target, propertyKey) {
91
91
  const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
92
92
  assertStageEntries('@UseFilter', 'filter', where, filters);
93
93
  // Decorators apply bottom-up, so a higher decorator's filters are tried first.
@@ -104,7 +104,7 @@ const ORDINALS = [
104
104
  ...existing
105
105
  ], target, propertyKey);
106
106
  }
107
- };
107
+ });
108
108
  }
109
109
 
110
110
  export { Catch, UseFilter };