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.
- package/CHANGELOG.md +362 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +365 -310
- package/dist/cjs/decorator/index.cjs +85 -61
- package/dist/cjs/testing/index.cjs +2236 -496
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +17 -4
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +27 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +84 -10
- package/dist/esm/common/route.js +21 -5
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +241 -179
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +45 -23
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +6 -7
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/startup-roots.js +23 -4
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +162 -83
- package/dist/esm/decorator/cooldown.decorator.js +20 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +11 -10
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +27 -11
- package/dist/esm/testing/cooldown-store-suite.js +85 -15
- package/dist/esm/testing/discord-defaults.js +249 -21
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +352 -205
- package/dist/esm/testing/mock-fn.js +173 -22
- package/dist/esm/testing/mock-interaction.js +1214 -209
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +65 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +3 -3
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +4 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +12 -1
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +10 -7
- package/dist/types/core/index.d.ts +10 -7
- package/dist/types/decorator/index.d.cts +31 -29
- package/dist/types/decorator/index.d.ts +31 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
- package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
- package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
- package/dist/types/testing/index.d.cts +406 -70
- package/dist/types/testing/index.d.ts +406 -70
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +4 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { handlerMethods, classLevelGuards,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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 };
|
package/dist/esm/package.json.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
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,
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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.
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
15
|
-
* calls at the limit where exactly one may pass
|
|
16
|
-
*
|
|
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.
|
|
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:
|
|
62
|
+
windowMs: 2000
|
|
59
63
|
};
|
|
60
64
|
await store.consume(key('sliding'), limit);
|
|
61
|
-
await sleep(
|
|
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,
|
|
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:
|
|
77
|
+
windowMs: 2000
|
|
74
78
|
};
|
|
79
|
+
const firstSent = Date.now();
|
|
75
80
|
await store.consume(key('retry'), limit);
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|