@velajs/vela 1.4.0 → 1.5.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.
- package/CHANGELOG.md +13 -1
- package/README.md +52 -0
- package/dist/cache/cache.module.d.ts +1 -0
- package/dist/cache/cache.module.js +6 -0
- package/dist/config/config.module.d.ts +4 -1
- package/dist/config/config.module.js +6 -0
- package/dist/container/container.d.ts +32 -4
- package/dist/container/container.js +252 -103
- package/dist/container/decorators.d.ts +8 -1
- package/dist/container/decorators.js +7 -4
- package/dist/container/index.d.ts +1 -1
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +13 -0
- package/dist/container/types.js +10 -0
- package/dist/cors/cors.module.d.ts +3 -1
- package/dist/cors/cors.module.js +2 -0
- package/dist/fetch/fetch.module.d.ts +3 -1
- package/dist/fetch/fetch.module.js +8 -3
- package/dist/http/argument-resolver.d.ts +10 -0
- package/dist/http/argument-resolver.js +89 -0
- package/dist/http/handler-executor.d.ts +20 -0
- package/dist/http/handler-executor.js +108 -0
- package/dist/http/instantiate.d.ts +4 -0
- package/dist/http/instantiate.js +18 -0
- package/dist/http/response-mapper.d.ts +8 -0
- package/dist/http/response-mapper.js +42 -0
- package/dist/http/route.manager.d.ts +1 -9
- package/dist/http/route.manager.js +23 -240
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/module/decorators.d.ts +6 -2
- package/dist/module/decorators.js +7 -6
- package/dist/module/index.d.ts +2 -1
- package/dist/module/index.js +2 -1
- package/dist/module/module-loader.d.ts +6 -1
- package/dist/module/module-loader.js +127 -47
- package/dist/module/stable-hash.d.ts +15 -0
- package/dist/module/stable-hash.js +52 -0
- package/dist/registry/types.d.ts +11 -0
- package/dist/throttler/throttler.module.d.ts +6 -2
- package/dist/throttler/throttler.module.js +6 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Scope } from "../constants.js";
|
|
2
|
-
import { ForwardRef, InjectionToken, ModuleVisibilityError } from "../container/types.js";
|
|
2
|
+
import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError } from "../container/types.js";
|
|
3
3
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
4
4
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
5
5
|
import { getOrCreateArray } from "../registry/util.js";
|
|
@@ -12,22 +12,43 @@ const APP_TOKENS = new Set([
|
|
|
12
12
|
APP_FILTER,
|
|
13
13
|
APP_MIDDLEWARE
|
|
14
14
|
]);
|
|
15
|
+
const DEFAULT_KEY = "default";
|
|
15
16
|
function isDynamicModule(value) {
|
|
17
|
+
// TS 4.9+ narrows `'module' in value` so `value.module` is typed `unknown`
|
|
18
|
+
// — no cast required for the typeof check below.
|
|
16
19
|
return typeof value === "object" && value !== null && "module" in value && typeof value.module === "function";
|
|
17
20
|
}
|
|
18
21
|
function implementsNestModule(cls) {
|
|
22
|
+
// `Type.prototype` is `any` — direct property access is type-safe enough.
|
|
19
23
|
return typeof cls.prototype?.configure === "function";
|
|
20
24
|
}
|
|
21
25
|
function tokenOfProvider(provider) {
|
|
22
26
|
return typeof provider === "function" ? provider : provider.provide;
|
|
23
27
|
}
|
|
28
|
+
function keyOfImport(entry) {
|
|
29
|
+
return isDynamicModule(entry) ? entry.key ?? DEFAULT_KEY : DEFAULT_KEY;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `ForwardRef.factory` is typed to return the broader `Token<T>` because the
|
|
33
|
+
* primitive is shared with provider injection. In module-imports position the
|
|
34
|
+
* runtime contract narrows: the factory must yield a module class or a
|
|
35
|
+
* `DynamicModule`. This helper validates the contract AND narrows the type
|
|
36
|
+
* without a structural cast.
|
|
37
|
+
*/ function unwrapModuleForwardRef(ref) {
|
|
38
|
+
const result = ref.factory();
|
|
39
|
+
if (typeof result === 'function') return result;
|
|
40
|
+
if (isDynamicModule(result)) return result;
|
|
41
|
+
throw new Error(`forwardRef in module imports must resolve to a module class or DynamicModule; ` + `got ${typeof result === 'object' ? 'a non-module object' : typeof result}.`);
|
|
42
|
+
}
|
|
24
43
|
export class ModuleLoader {
|
|
25
44
|
container;
|
|
26
45
|
router;
|
|
27
|
-
|
|
46
|
+
// class → set of keys already processed (multi-instance dedup is by both)
|
|
47
|
+
processedModules = new Map();
|
|
28
48
|
processingStack = new Set();
|
|
29
49
|
collectedControllers = new Set();
|
|
30
50
|
registeredProviders = [];
|
|
51
|
+
// (class, key) → exported tokens
|
|
31
52
|
moduleExportsCache = new Map();
|
|
32
53
|
globalExports = new Set();
|
|
33
54
|
consumerMiddlewareDefinitions = [];
|
|
@@ -54,7 +75,8 @@ export class ModuleLoader {
|
|
|
54
75
|
[]
|
|
55
76
|
]
|
|
56
77
|
]);
|
|
57
|
-
|
|
78
|
+
// (class, key) → composed moduleId (cached for stable identity within a load)
|
|
79
|
+
moduleIdByClassKey = new Map();
|
|
58
80
|
seenModuleIds = new Set();
|
|
59
81
|
constructor(container, router){
|
|
60
82
|
this.container = container;
|
|
@@ -66,47 +88,80 @@ export class ModuleLoader {
|
|
|
66
88
|
this.router.registerController(controller);
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
|
-
getModuleId(moduleClass) {
|
|
70
|
-
|
|
71
|
-
if (
|
|
91
|
+
getModuleId(moduleClass, key) {
|
|
92
|
+
let perClass = this.moduleIdByClassKey.get(moduleClass);
|
|
93
|
+
if (perClass) {
|
|
94
|
+
const cached = perClass.get(key);
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
} else {
|
|
97
|
+
perClass = new Map();
|
|
98
|
+
this.moduleIdByClassKey.set(moduleClass, perClass);
|
|
99
|
+
}
|
|
72
100
|
const baseName = moduleClass.name || "AnonModule";
|
|
73
|
-
let id = baseName
|
|
101
|
+
let id = `${baseName}#${key}`;
|
|
74
102
|
let counter = 0;
|
|
103
|
+
// Cross-class name collision (rare): two different classes named identically.
|
|
104
|
+
// The (class, key) pair identifies us; bump suffix to keep `id` strings unique
|
|
105
|
+
// in `seenModuleIds` so debug output stays unambiguous.
|
|
75
106
|
while(this.seenModuleIds.has(id)){
|
|
76
|
-
id = `${baseName}#${++counter}`;
|
|
107
|
+
id = `${baseName}#${key}~${++counter}`;
|
|
77
108
|
}
|
|
78
109
|
this.seenModuleIds.add(id);
|
|
79
|
-
|
|
110
|
+
perClass.set(key, id);
|
|
80
111
|
return id;
|
|
81
112
|
}
|
|
113
|
+
isProcessed(moduleClass, key) {
|
|
114
|
+
return this.processedModules.get(moduleClass)?.has(key) ?? false;
|
|
115
|
+
}
|
|
116
|
+
markProcessed(moduleClass, key) {
|
|
117
|
+
let keys = this.processedModules.get(moduleClass);
|
|
118
|
+
if (!keys) {
|
|
119
|
+
keys = new Set();
|
|
120
|
+
this.processedModules.set(moduleClass, keys);
|
|
121
|
+
}
|
|
122
|
+
keys.add(key);
|
|
123
|
+
}
|
|
124
|
+
getCachedExports(moduleClass, key) {
|
|
125
|
+
return this.moduleExportsCache.get(moduleClass)?.get(key);
|
|
126
|
+
}
|
|
127
|
+
cacheExports(moduleClass, key, exports) {
|
|
128
|
+
let perClass = this.moduleExportsCache.get(moduleClass);
|
|
129
|
+
if (!perClass) {
|
|
130
|
+
perClass = new Map();
|
|
131
|
+
this.moduleExportsCache.set(moduleClass, perClass);
|
|
132
|
+
}
|
|
133
|
+
perClass.set(key, exports);
|
|
134
|
+
}
|
|
82
135
|
processModule(moduleClassOrDynamic) {
|
|
83
|
-
// Handle dynamic modules ({ module, imports, controllers, providers })
|
|
84
136
|
let moduleClass;
|
|
85
137
|
let extraImports = [];
|
|
86
138
|
let extraControllers = [];
|
|
87
139
|
let extraProviders = [];
|
|
88
140
|
let extraExports = [];
|
|
141
|
+
let key = DEFAULT_KEY;
|
|
89
142
|
if (isDynamicModule(moduleClassOrDynamic)) {
|
|
90
143
|
moduleClass = moduleClassOrDynamic.module;
|
|
91
144
|
extraImports = moduleClassOrDynamic.imports ?? [];
|
|
92
145
|
extraControllers = moduleClassOrDynamic.controllers ?? [];
|
|
93
146
|
extraProviders = moduleClassOrDynamic.providers ?? [];
|
|
94
147
|
extraExports = moduleClassOrDynamic.exports ?? [];
|
|
148
|
+
key = moduleClassOrDynamic.key ?? DEFAULT_KEY;
|
|
95
149
|
} else {
|
|
96
150
|
moduleClass = moduleClassOrDynamic;
|
|
97
151
|
}
|
|
98
|
-
if (this.
|
|
152
|
+
if (this.isProcessed(moduleClass, key)) {
|
|
99
153
|
// Even if already processed, still collect extra controllers from dynamic module
|
|
100
154
|
for (const controller of extraControllers){
|
|
101
155
|
this.collectedControllers.add(controller);
|
|
102
156
|
}
|
|
103
|
-
return this.
|
|
157
|
+
return this.getCachedExports(moduleClass, key) ?? new Set();
|
|
104
158
|
}
|
|
105
|
-
|
|
159
|
+
const stackKey = `${moduleClass.name}#${key}`;
|
|
160
|
+
if (this.processingStack.has(stackKey)) {
|
|
106
161
|
const chain = [
|
|
107
162
|
...this.processingStack,
|
|
108
|
-
|
|
109
|
-
].
|
|
163
|
+
stackKey
|
|
164
|
+
].join(" -> ");
|
|
110
165
|
throw new Error(`Circular module dependency detected: ${chain}`);
|
|
111
166
|
}
|
|
112
167
|
if (!isModule(moduleClass)) {
|
|
@@ -120,25 +175,35 @@ export class ModuleLoader {
|
|
|
120
175
|
if (!metadata) {
|
|
121
176
|
throw new Error(`Failed to get module metadata for ${moduleClass.name}`);
|
|
122
177
|
}
|
|
123
|
-
this.processingStack.add(
|
|
124
|
-
const moduleId = this.getModuleId(moduleClass);
|
|
178
|
+
this.processingStack.add(stackKey);
|
|
179
|
+
const moduleId = this.getModuleId(moduleClass, key);
|
|
125
180
|
try {
|
|
126
181
|
const importedProviders = new Set(this.globalExports);
|
|
127
182
|
// Determine importedModuleIds eagerly (before recursing) so child
|
|
128
183
|
// modules can be referenced in our scope's importedModules set.
|
|
129
184
|
const importedModuleIds = new Set();
|
|
130
|
-
|
|
185
|
+
// Track per-class keys seen in this imports array — emit a loader-time
|
|
186
|
+
// diagnostic when the same module class appears under both `"default"`
|
|
187
|
+
// and at least one explicit key (almost always user error).
|
|
188
|
+
const keysByClassInImports = new Map();
|
|
189
|
+
const allImports = [
|
|
131
190
|
...metadata.imports,
|
|
132
191
|
...extraImports
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const importedModule = isForwardRef ? entry.factory() : entry;
|
|
192
|
+
];
|
|
193
|
+
for (const entry of allImports){
|
|
194
|
+
const importedModule = entry instanceof ForwardRef ? unwrapModuleForwardRef(entry) : entry;
|
|
137
195
|
const importedModuleClass = isDynamicModule(importedModule) ? importedModule.module : importedModule;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
196
|
+
const importedKey = keyOfImport(importedModule);
|
|
197
|
+
let keys = keysByClassInImports.get(importedModuleClass);
|
|
198
|
+
if (!keys) {
|
|
199
|
+
keys = new Set();
|
|
200
|
+
keysByClassInImports.set(importedModuleClass, keys);
|
|
201
|
+
}
|
|
202
|
+
keys.add(importedKey);
|
|
203
|
+
const importedId = this.getModuleId(importedModuleClass, importedKey);
|
|
204
|
+
importedModuleIds.add(importedId);
|
|
205
|
+
const importedStackKey = `${importedModuleClass.name}#${importedKey}`;
|
|
206
|
+
if (entry instanceof ForwardRef && this.processingStack.has(importedStackKey)) {
|
|
142
207
|
continue;
|
|
143
208
|
}
|
|
144
209
|
const exportedTokens = this.processModule(importedModule);
|
|
@@ -146,6 +211,7 @@ export class ModuleLoader {
|
|
|
146
211
|
importedProviders.add(token);
|
|
147
212
|
}
|
|
148
213
|
}
|
|
214
|
+
this.warnOnMixedDefaultAndKeyed(moduleClass.name, keysByClassInImports);
|
|
149
215
|
const allProviders = [
|
|
150
216
|
...metadata.providers,
|
|
151
217
|
...extraProviders
|
|
@@ -183,15 +249,18 @@ export class ModuleLoader {
|
|
|
183
249
|
this.registerProvider(provider, moduleId);
|
|
184
250
|
}
|
|
185
251
|
for (const controller of allControllers){
|
|
252
|
+
// Register the controller in its owning module's bucket so its
|
|
253
|
+
// dependencies resolve from the module's POV (vs `__root__`'s).
|
|
254
|
+
this.container.register(controller, moduleId);
|
|
186
255
|
this.collectedControllers.add(controller);
|
|
187
256
|
}
|
|
188
257
|
// Propagate module-level Use* decorators (@UseGuards, @UseInterceptors, etc.) to each controller
|
|
189
258
|
for (const controller of allControllers){
|
|
190
259
|
MetadataRegistry.propagateControllerComponents(moduleClass, controller);
|
|
191
260
|
}
|
|
192
|
-
this.
|
|
261
|
+
this.markProcessed(moduleClass, key);
|
|
193
262
|
const exports = this.buildExportSet(allExports, allProviders, importedProviders);
|
|
194
|
-
this.
|
|
263
|
+
this.cacheExports(moduleClass, key, exports);
|
|
195
264
|
if (isGlobal) {
|
|
196
265
|
for (const token of exports){
|
|
197
266
|
this.globalExports.add(token);
|
|
@@ -200,7 +269,7 @@ export class ModuleLoader {
|
|
|
200
269
|
// Call configure() if the module implements NestModule. The module is
|
|
201
270
|
// resolved through the container so it can have its own DI dependencies.
|
|
202
271
|
if (implementsNestModule(moduleClass)) {
|
|
203
|
-
if (!this.container.
|
|
272
|
+
if (!this.container.hasInScope(moduleClass, moduleId)) {
|
|
204
273
|
this.container.register(moduleClass, moduleId);
|
|
205
274
|
}
|
|
206
275
|
const instance = this.container.resolve(moduleClass, moduleId);
|
|
@@ -210,15 +279,26 @@ export class ModuleLoader {
|
|
|
210
279
|
}
|
|
211
280
|
return exports;
|
|
212
281
|
} finally{
|
|
213
|
-
this.processingStack.delete(
|
|
282
|
+
this.processingStack.delete(stackKey);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
warnOnMixedDefaultAndKeyed(parentName, keysByClass) {
|
|
286
|
+
const mode = this.container.getDiagnostics();
|
|
287
|
+
if (mode === "silent") return;
|
|
288
|
+
for (const [cls, keys] of keysByClass){
|
|
289
|
+
if (keys.size < 2) continue;
|
|
290
|
+
if (!keys.has(DEFAULT_KEY)) continue;
|
|
291
|
+
const message = `[vela] ${cls.name} imported in both bare and keyed form in '${parentName}'. ` + `These resolve to distinct module instances; consumers asking for an exported ` + `token will hit MultipleProvidersFoundError. Use one form consistently.`;
|
|
292
|
+
if (mode === "throw") throw new Error(message);
|
|
293
|
+
console.warn(message);
|
|
214
294
|
}
|
|
215
295
|
}
|
|
216
296
|
registerProvider(provider, moduleId) {
|
|
217
297
|
if (typeof provider === "function") {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
298
|
+
// Per-module bucket: the same class can be registered in multiple
|
|
299
|
+
// modules' buckets simultaneously without collision.
|
|
300
|
+
this.container.register(provider, moduleId);
|
|
301
|
+
this.registeredProviders.push(provider);
|
|
222
302
|
} else {
|
|
223
303
|
const token = provider.provide;
|
|
224
304
|
if (!token) {
|
|
@@ -226,22 +306,21 @@ export class ModuleLoader {
|
|
|
226
306
|
return;
|
|
227
307
|
}
|
|
228
308
|
if (this.isAppToken(token)) {
|
|
309
|
+
// Multiple APP_* providers within the same module bucket need
|
|
310
|
+
// distinct tokens so they don't overwrite each other in the bucket
|
|
311
|
+
// Map. Across buckets, `container.resolveAll(APP_GUARD)` walks
|
|
312
|
+
// every bucket — no need to mark synthetic tokens global.
|
|
229
313
|
const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
|
|
230
314
|
this.container.register({
|
|
231
315
|
...provider,
|
|
232
316
|
provide: syntheticToken
|
|
233
317
|
}, moduleId);
|
|
234
|
-
// Synthetic APP_* tokens are resolved at request time by RouteManager
|
|
235
|
-
// with no requester; mark them global so the visibility check passes.
|
|
236
|
-
this.container.markGlobalToken(syntheticToken);
|
|
237
318
|
this.registeredProviders.push(syntheticToken);
|
|
238
319
|
getOrCreateArray(this.appProviderTokens, token).push(syntheticToken);
|
|
239
320
|
return;
|
|
240
321
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.registeredProviders.push(token);
|
|
244
|
-
}
|
|
322
|
+
this.container.register(provider, moduleId);
|
|
323
|
+
this.registeredProviders.push(token);
|
|
245
324
|
}
|
|
246
325
|
}
|
|
247
326
|
isAppToken(token) {
|
|
@@ -258,7 +337,7 @@ export class ModuleLoader {
|
|
|
258
337
|
const isLocalProvider = providerTokens.has(exported);
|
|
259
338
|
const isImportedProvider = importedProviders.has(exported);
|
|
260
339
|
if (!isLocalProvider && !isImportedProvider) {
|
|
261
|
-
const name = exported.name
|
|
340
|
+
const name = typeof exported === 'function' ? exported.name : String(exported);
|
|
262
341
|
console.warn(`Warning: Exporting '${name}' which is neither a local provider nor imported from another module.`);
|
|
263
342
|
}
|
|
264
343
|
exportSet.add(exported);
|
|
@@ -295,9 +374,9 @@ export class ModuleLoader {
|
|
|
295
374
|
const instance = await this.container.resolveAsync(token);
|
|
296
375
|
instanceSet.add(instance);
|
|
297
376
|
} catch (err) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
377
|
+
if (err instanceof ModuleVisibilityError || err instanceof MultipleProvidersFoundError) {
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
301
380
|
this.routeError(err, `resolve provider`);
|
|
302
381
|
}
|
|
303
382
|
}
|
|
@@ -309,7 +388,9 @@ export class ModuleLoader {
|
|
|
309
388
|
const instance = await this.container.resolveAsync(controller);
|
|
310
389
|
instanceSet.add(instance);
|
|
311
390
|
} catch (err) {
|
|
312
|
-
if (err instanceof ModuleVisibilityError
|
|
391
|
+
if (err instanceof ModuleVisibilityError || err instanceof MultipleProvidersFoundError) {
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
313
394
|
this.routeError(err, `resolve controller ${controller.name}`);
|
|
314
395
|
}
|
|
315
396
|
}
|
|
@@ -323,7 +404,6 @@ export class ModuleLoader {
|
|
|
323
404
|
if (mode === "throw") {
|
|
324
405
|
throw err instanceof Error ? err : new Error(String(err));
|
|
325
406
|
}
|
|
326
|
-
// 'log'
|
|
327
407
|
console.warn(`[vela] ${context} failed:`, err);
|
|
328
408
|
}
|
|
329
409
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, sync, edge-safe hash for module-instance discrimination.
|
|
3
|
+
*
|
|
4
|
+
* Walks values (objects/arrays/primitives/functions/class instances) into a
|
|
5
|
+
* sorted, source-fingerprinting JSON string, then runs FNV-1a (32-bit) for a
|
|
6
|
+
* compact 8-hex-char output.
|
|
7
|
+
*
|
|
8
|
+
* Functions and class instances are tolerated — they become `__fn:source` or
|
|
9
|
+
* `__cls:ClassName` markers. Two distinct closures with the same source code
|
|
10
|
+
* collide; module authors who care about that should pass an explicit `key`.
|
|
11
|
+
*
|
|
12
|
+
* Not cryptographic. Sufficient for discriminating module instances within
|
|
13
|
+
* an app.
|
|
14
|
+
*/
|
|
15
|
+
export declare function stableHash(value: unknown): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, sync, edge-safe hash for module-instance discrimination.
|
|
3
|
+
*
|
|
4
|
+
* Walks values (objects/arrays/primitives/functions/class instances) into a
|
|
5
|
+
* sorted, source-fingerprinting JSON string, then runs FNV-1a (32-bit) for a
|
|
6
|
+
* compact 8-hex-char output.
|
|
7
|
+
*
|
|
8
|
+
* Functions and class instances are tolerated — they become `__fn:source` or
|
|
9
|
+
* `__cls:ClassName` markers. Two distinct closures with the same source code
|
|
10
|
+
* collide; module authors who care about that should pass an explicit `key`.
|
|
11
|
+
*
|
|
12
|
+
* Not cryptographic. Sufficient for discriminating module instances within
|
|
13
|
+
* an app.
|
|
14
|
+
*/ export function stableHash(value) {
|
|
15
|
+
return fnv1a(stableStringify(value));
|
|
16
|
+
}
|
|
17
|
+
function stableStringify(value) {
|
|
18
|
+
if (value === null || value === undefined) return JSON.stringify(value) ?? 'undefined';
|
|
19
|
+
if (typeof value === 'function') {
|
|
20
|
+
// Source-fingerprint via FNV-1a so we don't bloat the parent hash; this
|
|
21
|
+
// keeps two distinct factory functions distinguishable as long as their
|
|
22
|
+
// source differs (the common case). Names are intentionally excluded —
|
|
23
|
+
// two arrow functions with the same body should dedup.
|
|
24
|
+
return `__fn:${fnv1a(value.toString())}`;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'symbol') {
|
|
27
|
+
return `__sym:${value.description ?? ''}`;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== 'object') return JSON.stringify(value);
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
32
|
+
}
|
|
33
|
+
const proto = Object.getPrototypeOf(value);
|
|
34
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
35
|
+
const name = proto?.constructor?.name ?? 'AnonymousClass';
|
|
36
|
+
return `__cls:${name}` + stringifyOwnProps(value);
|
|
37
|
+
}
|
|
38
|
+
return stringifyOwnProps(value);
|
|
39
|
+
}
|
|
40
|
+
function stringifyOwnProps(obj) {
|
|
41
|
+
const keys = Object.keys(obj).sort();
|
|
42
|
+
const parts = keys.map((k)=>JSON.stringify(k) + ':' + stableStringify(obj[k]));
|
|
43
|
+
return '{' + parts.join(',') + '}';
|
|
44
|
+
}
|
|
45
|
+
function fnv1a(s) {
|
|
46
|
+
let h = 0x811c9dc5;
|
|
47
|
+
for(let i = 0; i < s.length; i++){
|
|
48
|
+
h ^= s.charCodeAt(i);
|
|
49
|
+
h = Math.imul(h, 0x01000193);
|
|
50
|
+
}
|
|
51
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
52
|
+
}
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -45,6 +45,17 @@ export interface AsyncModuleOptions<T = unknown> {
|
|
|
45
45
|
}
|
|
46
46
|
export interface DynamicModule {
|
|
47
47
|
module: Type;
|
|
48
|
+
/**
|
|
49
|
+
* Author-supplied instance discriminator. Two DynamicModules with the same
|
|
50
|
+
* `module` class and the same `key` dedup; with different keys they coexist
|
|
51
|
+
* as separate module instances. Defaults to `"default"` when absent — which
|
|
52
|
+
* preserves single-instance dedup for the common case.
|
|
53
|
+
*
|
|
54
|
+
* For `forRoot(options)`, derive `key: stableHash(options)` so identical
|
|
55
|
+
* options dedup automatically. For `forRootAsync` (factories aren't
|
|
56
|
+
* structurally hashable), pass an explicit string.
|
|
57
|
+
*/
|
|
58
|
+
key?: string;
|
|
48
59
|
imports?: ModuleImport[];
|
|
49
60
|
providers?: Array<Type | ProviderOptions>;
|
|
50
61
|
controllers?: Type[];
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AsyncModuleOptions, DynamicModule } from '../module/types';
|
|
2
2
|
import type { ThrottlerModuleOptions } from './throttler.types';
|
|
3
3
|
export declare class ThrottlerModule {
|
|
4
|
-
static forRoot(options: ThrottlerModuleOptions
|
|
5
|
-
|
|
4
|
+
static forRoot(options: ThrottlerModuleOptions & {
|
|
5
|
+
key?: string;
|
|
6
|
+
}): DynamicModule;
|
|
7
|
+
static forRootAsync(options: AsyncModuleOptions<ThrottlerModuleOptions> & {
|
|
8
|
+
key?: string;
|
|
9
|
+
}): DynamicModule;
|
|
6
10
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stableHash } from "../module/stable-hash.js";
|
|
1
2
|
import { APP_GUARD } from "../pipeline/tokens.js";
|
|
2
3
|
import { ThrottlerGuard } from "./throttler.guard.js";
|
|
3
4
|
import { ThrottlerStorage } from "./throttler.storage.js";
|
|
@@ -21,6 +22,7 @@ export class ThrottlerModule {
|
|
|
21
22
|
];
|
|
22
23
|
return {
|
|
23
24
|
module: ThrottlerModule,
|
|
25
|
+
key: options.key ?? stableHash(options),
|
|
24
26
|
providers,
|
|
25
27
|
exports: [
|
|
26
28
|
THROTTLER_OPTIONS,
|
|
@@ -51,6 +53,10 @@ export class ThrottlerModule {
|
|
|
51
53
|
];
|
|
52
54
|
return {
|
|
53
55
|
module: ThrottlerModule,
|
|
56
|
+
key: options.key ?? stableHash({
|
|
57
|
+
inject: options.inject,
|
|
58
|
+
useFactory: options.useFactory
|
|
59
|
+
}),
|
|
54
60
|
imports: options.imports ?? [],
|
|
55
61
|
providers,
|
|
56
62
|
exports: [
|