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,9 +1,10 @@
1
1
  import 'reflect-metadata';
2
- import { handlerMethods, classLevelGuards, classChain, consumeDispatchMark, runDirectCall, declaringPrototype } from '../core/guard-runner.js';
2
+ import { handlerMethods, classLevelGuards, consumeDispatchMark, runDirectCall, declaringPrototype, classChain } from '../core/guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
+ import { wrapsMethod } from '../util/method-wrapper.util.js';
4
5
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
5
6
  import { META } from '../util/metadata-keys.js';
6
- import { deprecatedOnMethod } from '../util/refusal.util.js';
7
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
7
8
 
8
9
  /**
9
10
  * Adds guards to a method's class or method list, then republishes the effective list under
@@ -34,10 +35,10 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
34
35
  * which has already run the guards, passes through every wrapper.
35
36
  */ function applyGuards(descriptor, prototype, propertyKey) {
36
37
  const originalMethod = descriptor.value;
37
- descriptor.value = async function(...args) {
38
+ descriptor.value = wrapsMethod(async function(...args) {
38
39
  if (consumeDispatchMark(args[0], this, propertyKey)) return originalMethod.apply(this, args);
39
40
  return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args));
40
- };
41
+ }, originalMethod);
41
42
  const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
42
43
  Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
43
44
  }
@@ -119,14 +120,14 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
119
120
  * @see {@link UseGuard}
120
121
  * @see {@link GuardDeniedError}
121
122
  * @see {@link ExecutionContext}
122
- * @see {@link https://meocord.dev/docs/4.1/guards | Guards}
123
+ * @see {@link https://meocord.dev/docs/4.2/guards | Guards}
123
124
  */ function Guard(options = {}) {
124
- return function(target, propertyKey) {
125
+ return declaring(function(target, propertyKey) {
125
126
  if (deprecatedOnMethod('@Guard', target, propertyKey)) return;
126
127
  makeInjectable(target);
127
128
  defineStageTypes(target, options.types, 'Guard');
128
129
  Reflect.defineMetadata(META.guardClass, true, target);
129
- };
130
+ });
130
131
  }
131
132
  /**
132
133
  * Runs guards before a handler, or before every handler of a controller.
@@ -157,10 +158,10 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
157
158
  * @see {@link Guard}
158
159
  * @see {@link GuardDeniedError}
159
160
  * @see {@link ExecutionContext}
160
- * @see {@link https://meocord.dev/docs/4.1/guards | Guards}
161
+ * @see {@link https://meocord.dev/docs/4.2/guards | Guards}
161
162
  */ function UseGuard(...entries) {
162
163
  const guards = entries;
163
- return function(target, propertyKey, descriptor) {
164
+ return declaring(function(target, propertyKey, descriptor) {
164
165
  const where = propertyKey === undefined ? String(target?.name) : `${target.constructor.name}.${String(propertyKey)}`;
165
166
  assertStageEntries('@UseGuard', 'guard', where, guards);
166
167
  if (descriptor && propertyKey) {
@@ -185,7 +186,7 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
185
186
  }
186
187
  }
187
188
  }
188
- };
189
+ });
189
190
  }
190
191
 
191
192
  export { Guard, UseGuard, guardOwnHandlersWithBaseGuards };
@@ -1,5 +1,5 @@
1
1
  import { inject } from 'inversify';
2
- import { refuseOnClass } from '../util/refusal.util.js';
2
+ import { declaring, refuseOnClass } from '../util/refusal.util.js';
3
3
 
4
4
  /**
5
5
  * Injects what a token provides into a constructor parameter.
@@ -24,14 +24,14 @@ import { refuseOnClass } from '../util/refusal.util.js';
24
24
  * @category Params
25
25
  * @see {@link Service}
26
26
  * @see {@link createToken}
27
- * @see {@link https://meocord.dev/docs/4.1/services | Services}
27
+ * @see {@link https://meocord.dev/docs/4.2/services | Services}
28
28
  */ function Inject(token) {
29
29
  const decorate = inject(token);
30
- return (target, propertyKey, parameterIndex)=>{
30
+ return declaring((target, propertyKey, parameterIndex)=>{
31
31
  // A constructor parameter has no property key either, but has an index
32
32
  if (typeof parameterIndex !== 'number') refuseOnClass('@Inject', target, propertyKey, 'a constructor parameter or a property');
33
33
  decorate(target, propertyKey, parameterIndex);
34
- };
34
+ });
35
35
  }
36
36
 
37
37
  export { Inject };
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
- import { refuseOnMethod } from '../util/refusal.util.js';
5
+ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
6
6
 
7
7
  /**
8
8
  * Marks a class as an interceptor, which wraps a handler to act before and after it.
@@ -36,13 +36,13 @@ import { refuseOnMethod } from '../util/refusal.util.js';
36
36
  * @group Decorators
37
37
  * @category Pipeline stages
38
38
  * @see {@link UseInterceptor}
39
- * @see {@link https://meocord.dev/docs/4.1/interceptors | Interceptors}
39
+ * @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
40
40
  */ function Interceptor(options = {}) {
41
- return function(target, propertyKey) {
41
+ return declaring(function(target, propertyKey) {
42
42
  refuseOnMethod('@Interceptor', target, propertyKey);
43
43
  makeInjectable(target);
44
44
  defineStageTypes(target, options.types, 'Interceptor');
45
- };
45
+ });
46
46
  }
47
47
  /**
48
48
  * Runs interceptors around a handler, or around every handler of a controller.
@@ -73,9 +73,9 @@ import { refuseOnMethod } from '../util/refusal.util.js';
73
73
  * @group Decorators
74
74
  * @category Pipeline stages
75
75
  * @see {@link Interceptor}
76
- * @see {@link https://meocord.dev/docs/4.1/interceptors | Interceptors}
76
+ * @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
77
77
  */ function UseInterceptor(...interceptors) {
78
- return function(target, propertyKey) {
78
+ return declaring(function(target, propertyKey) {
79
79
  const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
80
80
  assertStageEntries('@UseInterceptor', 'interceptor', where, interceptors);
81
81
  // Decorators apply bottom-up, so a higher decorator's interceptors go first, as with @UseGuard.
@@ -92,7 +92,7 @@ import { refuseOnMethod } from '../util/refusal.util.js';
92
92
  ...existing
93
93
  ], target, propertyKey);
94
94
  }
95
- };
95
+ });
96
96
  }
97
97
 
98
98
  export { Interceptor, UseInterceptor };
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { defineStageTypes } from '../core/stage-scope.js';
4
- import { refuseOnMethod, refuse } from '../util/refusal.util.js';
4
+ import { declaring, refuseOnMethod, refuse } from '../util/refusal.util.js';
5
5
  import { META } from '../util/metadata-keys.js';
6
6
 
7
7
  /**
@@ -35,9 +35,9 @@ import { META } from '../util/metadata-keys.js';
35
35
  * @pipeline observers-settled once the call has settled and been answered, however it ended
36
36
  * @group Decorators
37
37
  * @category Pipeline stages
38
- * @see {@link https://meocord.dev/docs/4.1/observers | Observers}
38
+ * @see {@link https://meocord.dev/docs/4.2/observers | Observers}
39
39
  */ function Observer(options = {}) {
40
- return function(target, propertyKey) {
40
+ return declaring(function(target, propertyKey) {
41
41
  refuseOnMethod('@Observer', target, propertyKey);
42
42
  if (typeof target.prototype.onSettled !== 'function') {
43
43
  throw refuse(new Error(`${target.name}: an @Observer needs an onSettled method, and it has none.`));
@@ -45,7 +45,7 @@ import { META } from '../util/metadata-keys.js';
45
45
  makeInjectable(target);
46
46
  defineStageTypes(target, options.types, 'Observer');
47
47
  Reflect.defineMetadata(META.observerClass, true, target);
48
- };
48
+ });
49
49
  }
50
50
 
51
51
  export { Observer };
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
- import { deprecatedOnMethod } from '../util/refusal.util.js';
3
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
4
4
 
5
5
  /**
6
6
  * Marks a class as a service, which controllers and other services inject by its type.
@@ -27,12 +27,12 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
27
27
  * @group Decorators
28
28
  * @category App
29
29
  * @see {@link Inject}
30
- * @see {@link https://meocord.dev/docs/4.1/services | Services}
30
+ * @see {@link https://meocord.dev/docs/4.2/services | Services}
31
31
  */ function Service() {
32
- return function(target, propertyKey) {
32
+ return declaring(function(target, propertyKey) {
33
33
  if (deprecatedOnMethod('@Service', target, propertyKey)) return;
34
34
  makeInjectable(target);
35
- };
35
+ });
36
36
  }
37
37
 
38
38
  export { Service };
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { assertValidTheme } from '../core/theme-validation.js';
3
3
  import { THEMED_CLASSES } from '../core/theme-runtime.js';
4
4
  import { copyLayer } from '../core/theme-scope.js';
5
- import { refuse } from '../util/refusal.util.js';
5
+ import { declaring, refuse } from '../util/refusal.util.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
7
 
8
8
  /**
@@ -35,7 +35,7 @@ import { META } from '../util/metadata-keys.js';
35
35
  * @group Decorators
36
36
  * @category Controllers
37
37
  */ function UseTheme(theme) {
38
- return function(target, propertyKey) {
38
+ return declaring(function(target, propertyKey) {
39
39
  const onClass = propertyKey === undefined;
40
40
  const owner = onClass ? target : target.constructor;
41
41
  const where = `${owner.name}${onClass ? '' : `.${String(propertyKey)}`}: @UseTheme`;
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
47
47
  if (onClass) Reflect.defineMetadata(key, layer, target);
48
48
  else Reflect.defineMetadata(key, layer, target, propertyKey);
49
49
  THEMED_CLASSES.add(owner);
50
- };
50
+ });
51
51
  }
52
52
 
53
53
  export { UseTheme };
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { assertStageEntries } from '../core/stage-scope.js';
4
- import { refuseOnMethod, refuseOnClass, refuse } from '../util/refusal.util.js';
4
+ import { declaring, refuseOnClass, refuse, refuseOnMethod } from '../util/refusal.util.js';
5
5
  import { META } from '../util/metadata-keys.js';
6
6
 
7
7
  /**
@@ -37,9 +37,9 @@ import { META } from '../util/metadata-keys.js';
37
37
  * @category Pipeline stages
38
38
  * @see {@link UsePipe}
39
39
  * @see {@link ValidationError}
40
- * @see {@link https://meocord.dev/docs/4.1/validation | Validation and pipes}
40
+ * @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
41
41
  */ function Validate(schema, options = {}) {
42
- return function(target, propertyKey, _descriptor) {
42
+ return declaring(function(target, propertyKey, _descriptor) {
43
43
  refuseOnClass('@Validate', target, propertyKey);
44
44
  // Checked where it applies, so the refusal names the handler
45
45
  if (typeof schema?.['~standard']?.validate !== 'function') {
@@ -57,7 +57,7 @@ import { META } from '../util/metadata-keys.js';
57
57
  pipes: options.pipes ?? {}
58
58
  };
59
59
  Reflect.defineMetadata(META.methodValidation, metadata, target, propertyKey);
60
- };
60
+ });
61
61
  }
62
62
  /**
63
63
  * Runs pipes on one value of a handler's input, to turn it into what the handler works with.
@@ -89,9 +89,9 @@ import { META } from '../util/metadata-keys.js';
89
89
  * @category Pipeline stages
90
90
  * @see {@link Pipe}
91
91
  * @see {@link Validate}
92
- * @see {@link https://meocord.dev/docs/4.1/validation | Validation and pipes}
92
+ * @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
93
93
  */ function UsePipe(key, ...pipes) {
94
- return function(target, propertyKey, _descriptor) {
94
+ return declaring(function(target, propertyKey, _descriptor) {
95
95
  refuseOnClass('@UsePipe', target, propertyKey);
96
96
  assertStageEntries('@UsePipe', 'pipe', `${target.constructor.name}.${propertyKey}`, pipes);
97
97
  // Decorators apply bottom-up, so a higher @UsePipe's pipes go first, in the order they read.
@@ -103,7 +103,7 @@ import { META } from '../util/metadata-keys.js';
103
103
  })),
104
104
  ...existing
105
105
  ], target, propertyKey);
106
- };
106
+ });
107
107
  }
108
108
  /**
109
109
  * Marks a class as a pipe, which turns one input value into what the handler works with.
@@ -129,12 +129,12 @@ import { META } from '../util/metadata-keys.js';
129
129
  * @group Decorators
130
130
  * @category Pipeline stages
131
131
  * @see {@link UsePipe}
132
- * @see {@link https://meocord.dev/docs/4.1/validation | Validation and pipes}
132
+ * @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
133
133
  */ function Pipe() {
134
- return function(target, propertyKey) {
134
+ return declaring(function(target, propertyKey) {
135
135
  refuseOnMethod('@Pipe', target, propertyKey);
136
136
  makeInjectable(target);
137
- };
137
+ });
138
138
  }
139
139
 
140
140
  export { Pipe, UsePipe, Validate };
@@ -1,4 +1,4 @@
1
- var version = "4.1.2";
1
+ var version = "4.2.1";
2
2
  var engines = {
3
3
  node: ">=22.13"
4
4
  };
@@ -1,7 +1,15 @@
1
1
  import { CATALOGS, placeholderNames } from '../common/translator.js';
2
2
  import { MEOCORD_MESSAGES } from '../common/meocord-messages.js';
3
3
 
4
- const isPlural = (value)=>typeof value === 'object' && value !== null && typeof value.other === 'string';
4
+ const PLURAL_CATEGORIES = new Set([
5
+ 'zero',
6
+ 'one',
7
+ 'two',
8
+ 'few',
9
+ 'many',
10
+ 'other'
11
+ ]);
12
+ /** A plural: an object whose every key is a plural category, as the translator's types read one; any other is a group. */ const isPlural = (value)=>typeof value === 'object' && value !== null && Object.keys(value).length > 0 && Object.keys(value).every((key)=>PLURAL_CATEGORIES.has(key));
5
13
  /** Every message key of a catalog, and whether it is a plural. */ function leaves(catalog, prefix = '') {
6
14
  return Object.entries(catalog).flatMap(([name, value])=>{
7
15
  const key = `${prefix}${name}`;
@@ -20,8 +28,9 @@ const isPlural = (value)=>typeof value === 'object' && value !== null && typeof
20
28
  return leaves(value, `${key}.`);
21
29
  });
22
30
  }
23
- function at(catalog, key) {
24
- return key.split('.').reduce((current, part)=>current?.[part], catalog);
31
+ /** A translation's text, which an empty string isn't: the translator reads `''` as missing, as tools export one untranslated. */ const isText = (value)=>typeof value === 'string' && value !== '';
32
+ /** The value at a key's path, through own properties only, so `constructor` or `toString` is not read off a prototype. */ function at(catalog, key) {
33
+ return key.split('.').reduce((current, part)=>typeof current === 'object' && current !== null && Object.hasOwn(current, part) ? current[part] : undefined, catalog);
25
34
  }
26
35
  const MEOCORD_KEYS = new Set(leaves(MEOCORD_MESSAGES).map(({ key })=>key));
27
36
  const isMeoCordKey = (key)=>key.startsWith('meocord.');
@@ -44,9 +53,11 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
44
53
  *
45
54
  * Use it in a test for a team that wants no fallback in production. It reports the messages a locale lacks, the
46
55
  * messages the default catalog doesn't have, the plural forms a language needs but a plural lacks, such as `few` for
47
- * Russian, and each `{param}` a translation uses that the message it translates doesn't take. It reads the catalogs'
48
- * own strings, so it checks the params of a catalog from a plain variable or a JSON file too, which the compiler
49
- * can't.
56
+ * Russian, a translation of another shape than the default's, a text for a plural or a plural for a text, and each
57
+ * `{param}` a translation uses that the message it translates doesn't take. An empty translation, `''`, counts as
58
+ * lacking, as the translator reads it as missing. It reads the catalogs' own strings, so it checks the params of a
59
+ * catalog from a plain variable or a JSON file too, which the compiler can't. An object counts as a plural only when
60
+ * its every key is a plural category, so a group with an `other` key is a group.
50
61
  *
51
62
  * @remarks
52
63
  * MeoCord's own texts, a catalog's `meocord` group, fall back to English by design, so only a key MeoCord lacks, and
@@ -79,7 +90,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
79
90
  * @group Testing
80
91
  * @category Inspection
81
92
  * @see {@link createTranslator}
82
- * @see {@link https://meocord.dev/docs/4.1/localisation | Localisation}
93
+ * @see {@link https://meocord.dev/docs/4.2/localisation | Localisation}
83
94
  */ function expectCompleteCatalog(translator, options = {}) {
84
95
  const catalogs = translator[CATALOGS];
85
96
  if (!catalogs) throw new Error('expectCompleteCatalog takes a translator made by createTranslator.');
@@ -93,12 +104,17 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
93
104
  const categories = new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
94
105
  for (const { key, plural } of reference){
95
106
  const message = at(catalog, key);
96
- if (message === undefined) {
107
+ if (message === undefined || message === '') {
97
108
  problems.push(`missing ${key}`);
98
109
  continue;
99
110
  }
100
- if (plural && isPlural(message)) {
101
- const missing = categories.filter((category)=>typeof message[category] !== 'string');
111
+ // A translation of another shape than the default's can't be read as it: a text for a plural, or the reverse
112
+ if (plural !== isPlural(message) || !plural && typeof message !== 'string') {
113
+ problems.push(`${key} should be a ${plural ? 'plural' : 'text'}, as the default is`);
114
+ continue;
115
+ }
116
+ if (plural) {
117
+ const missing = categories.filter((category)=>!isText(message[category]));
102
118
  if (missing.length > 0) problems.push(`${key} lacks ${missing.join(', ')}`);
103
119
  }
104
120
  const original = at(defaultCatalog, key);
@@ -106,7 +122,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
106
122
  for (const name of stray)problems.push(`${key} takes no {${name}}; the default is "${shown(original)}"`);
107
123
  }
108
124
  if (options.meocord && !locale.startsWith('en-')) {
109
- for (const key of MEOCORD_KEYS)if (at(catalog, key) === undefined) problems.push(`missing ${key}`);
125
+ for (const key of MEOCORD_KEYS)if (at(catalog, key) === undefined || at(catalog, key) === '') problems.push(`missing ${key}`);
110
126
  }
111
127
  for (const { key } of leaves(catalog)){
112
128
  if (isMeoCordKey(key)) {
@@ -3,6 +3,7 @@ import { CooldownStore } from '../common/cooldown-store.js';
3
3
 
4
4
  /** Long enough for a store over the network to answer each case, short enough that the suite takes seconds. */ const CASE_TIMEOUT_MS = 10000;
5
5
  const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
6
+ /** How far a store's clock may run from this process's, as a store's server keeps its own time. */ const CLOCK_SLACK_MS = 50;
6
7
  /**
7
8
  * Checks that a `CooldownStore` counts calls as `MemoryCooldownStore` does, as a suite of test cases.
8
9
  *
@@ -11,9 +12,11 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
11
12
  *
12
13
  * @remarks
13
14
  * The cases cover what a shared store most often gets wrong: a sliding window rather than fixed buckets,
14
- * `retryAfterMs` from the oldest call still in the window, calls in the same millisecond kept apart, and concurrent
15
- * calls at the limit where exactly one may pass. They use real time, with windows short enough that the suite takes
16
- * a few seconds, and each case counts under keys of its own. Each window is a whole number of milliseconds, as
15
+ * `retryAfterMs` until a use frees up, from the oldest of the newest `uses` calls in the window, calls in the same
16
+ * millisecond kept apart, concurrent calls at the limit where exactly one may pass, a batch refusal that names the
17
+ * longest wait, and a release that frees only the call it gave back. They use real time, with windows short enough that the suite takes
18
+ * a few seconds and margins wide enough that a correct store passes when timers fire late, as on a busy CI runner,
19
+ * and each case counts under keys of its own. Each window is a whole number of milliseconds, as
17
20
  * `@Cooldown` gives a store: it rounds its `seconds` to the millisecond.
18
21
  *
19
22
  * @param name - What the store is called in the report.
@@ -30,7 +33,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
30
33
  * @group Testing
31
34
  * @category Inspection
32
35
  * @see {@link CooldownStore}
33
- * @see {@link https://meocord.dev/docs/4.1/recipes/cooldown-stores | Cooldown stores}
36
+ * @see {@link https://meocord.dev/docs/4.2/recipes/cooldown-stores | Cooldown stores}
34
37
  */ function testCooldownStore(name, factory, { describe, it, expect }) {
35
38
  const run = randomUUID();
36
39
  const key = (label)=>`meocord-suite:${run}:${label}`;
@@ -53,32 +56,62 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
53
56
  expect(refused.retryAfterMs).toBeLessThanOrEqual(limit.windowMs);
54
57
  });
55
58
  test('slides its window: a call is allowed once the oldest leaves it, not when a bucket resets', async (store)=>{
59
+ // A second between calls, in a 2 s window, leaves room for a timer that fires late on a busy runner
56
60
  const limit = {
57
61
  uses: 2,
58
- windowMs: 500
62
+ windowMs: 2000
59
63
  };
60
64
  await store.consume(key('sliding'), limit);
61
- await sleep(250);
65
+ await sleep(1000);
62
66
  await store.consume(key('sliding'), limit);
63
67
  const refused = await store.consume(key('sliding'), limit);
64
68
  expect(refused.allowed).toBe(false);
65
69
  await sleep(refused.retryAfterMs + 40);
66
- // The first call has left the window; the second, 250ms younger, still holds its use.
70
+ // The first call has left the window; the second, a second younger, still holds its use.
67
71
  expect((await store.consume(key('sliding'), limit)).allowed).toBe(true);
68
72
  expect((await store.consume(key('sliding'), limit)).allowed).toBe(false);
69
73
  });
70
74
  test('counts retryAfterMs from the oldest call still in the window', async (store)=>{
71
75
  const limit = {
72
76
  uses: 2,
73
- windowMs: 1000
77
+ windowMs: 2000
74
78
  };
79
+ const firstSent = Date.now();
75
80
  await store.consume(key('retry'), limit);
76
- await sleep(300);
81
+ const firstDone = Date.now();
82
+ await sleep(1000);
83
+ const secondSent = Date.now();
77
84
  await store.consume(key('retry'), limit);
78
- // From the oldest call, about 700ms remain; from the newest, about 1000ms would.
79
85
  const { retryAfterMs } = await store.consume(key('retry'), limit);
80
- expect(retryAfterMs).toBeGreaterThan(limit.windowMs - 600);
81
- expect(retryAfterMs).toBeLessThanOrEqual(limit.windowMs - 200);
86
+ const thirdDone = Date.now();
87
+ // What is left of the oldest call's window, by the times measured around each call, as timers may fire late;
88
+ // counted from the newest call, nearly the whole window would be left instead
89
+ expect(retryAfterMs).toBeLessThanOrEqual(limit.windowMs - (secondSent - firstDone) + CLOCK_SLACK_MS);
90
+ expect(retryAfterMs).toBeGreaterThanOrEqual(limit.windowMs - (thirdDone - firstSent) - CLOCK_SLACK_MS);
91
+ });
92
+ // @Cooldown keeps the calls counted so far when a limit is lowered, so a key can hold more calls than `uses`
93
+ test('counts retryAfterMs from the oldest of the newest `uses` calls when a lowered limit leaves more in the window', async (store)=>{
94
+ const windowMs = 2000;
95
+ for(let call = 0; call < 3; call += 1)await store.consume(key('lowered'), {
96
+ uses: 5,
97
+ windowMs
98
+ });
99
+ await sleep(1000);
100
+ const newerSent = Date.now();
101
+ for(let call = 0; call < 2; call += 1)await store.consume(key('lowered'), {
102
+ uses: 5,
103
+ windowMs
104
+ });
105
+ const { allowed, retryAfterMs } = await store.consume(key('lowered'), {
106
+ uses: 2,
107
+ windowMs
108
+ });
109
+ const refusedDone = Date.now();
110
+ expect(allowed).toBe(false);
111
+ // A use frees up when the older of the two newest calls leaves the window; counted from the oldest call, it would
112
+ // be a second sooner
113
+ expect(retryAfterMs).toBeGreaterThanOrEqual(windowMs - (refusedDone - newerSent) - CLOCK_SLACK_MS);
114
+ expect(retryAfterMs).toBeLessThanOrEqual(windowMs + CLOCK_SLACK_MS);
82
115
  });
83
116
  test('counts each key on its own', async (store)=>{
84
117
  const limit = {
@@ -141,6 +174,41 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
141
174
  expect(refused.retryAfterMs).toBeGreaterThan(short.windowMs);
142
175
  expect(refused.retryAfterMs).toBeLessThanOrEqual(long.windowMs);
143
176
  });
177
+ // A refusal names the wait until every cooldown of the batch allows the call; the default consumeMany stops at the first
178
+ test('names the longest wait when several cooldowns refuse a batch, or with the default consumeMany, the first', async (store)=>{
179
+ const short = {
180
+ uses: 1,
181
+ windowMs: 1000
182
+ };
183
+ const long = {
184
+ uses: 1,
185
+ windowMs: 2000
186
+ };
187
+ const batch = [
188
+ {
189
+ key: key('both-short'),
190
+ limit: short
191
+ },
192
+ {
193
+ key: key('both-long'),
194
+ limit: long
195
+ }
196
+ ];
197
+ await store.consume(batch[0].key, short);
198
+ await store.consume(batch[1].key, long);
199
+ const refused = await store.consumeMany(batch);
200
+ expect(refused.allowed).toBe(false);
201
+ const [blocked, limit] = overridesConsumeMany(store) ? [
202
+ 1,
203
+ long
204
+ ] : [
205
+ 0,
206
+ short
207
+ ];
208
+ expect(refused.blocked).toBe(blocked);
209
+ expect(refused.retryAfterMs).toBeGreaterThan(limit.windowMs - 1000);
210
+ expect(refused.retryAfterMs).toBeLessThanOrEqual(limit.windowMs);
211
+ });
144
212
  // The default consumeMany counts in order and stops at the first refusal, so the entries before it count
145
213
  test('records nothing when one cooldown of a batch refuses it, or with the default consumeMany, counts those before it', async (store)=>{
146
214
  const limit = {
@@ -183,10 +251,11 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
183
251
  }, ()=>store.consumeMany(batch)));
184
252
  expect(verdicts.filter((verdict)=>verdict.allowed).length).toBe(1);
185
253
  });
186
- // A store without release keeps a counted call counted, as the default consumeMany does
187
- test('releases a call it counted when the verdict gives release, or keeps it counted without one', async (store)=>{
254
+ // A store without release keeps a counted call counted, as the default consumeMany does. Two uses tell a release of
255
+ // the one call apart from one that frees the key's other calls too
256
+ test('releases only the call it counted when the verdict gives release, or keeps it counted without one', async (store)=>{
188
257
  const limit = {
189
- uses: 1,
258
+ uses: 2,
190
259
  windowMs: 2000
191
260
  };
192
261
  const entries = [
@@ -199,6 +268,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
199
268
  limit
200
269
  }
201
270
  ];
271
+ expect((await store.consumeMany(entries)).allowed).toBe(true);
202
272
  const counted = await store.consumeMany(entries);
203
273
  expect(counted.allowed).toBe(true);
204
274
  if (!counted.release) {