@velajs/vela 1.4.1 → 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 +10 -1
- package/README.md +39 -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/route.manager.js +3 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
A sanctioned per-request injectable lands as a framework primitive,
|
|
5
|
+
A sanctioned per-request injectable lands as a framework primitive, the metadata-store unification finally has its regression tests, and dynamic module identity becomes consistent — closing the last open audit item.
|
|
6
6
|
|
|
7
7
|
### New
|
|
8
8
|
|
|
9
|
+
- **Dynamic module identity is now first-class** (audit #2 — last open item, fully closed). `DynamicModule` gains an optional `key?: string`; module authors call `key: stableHash(options)` inside `forRoot()` so two distinct option sets register as distinct module instances. The DI container is bucketed per-module (`Map<moduleId, Map<Token, Registration>>`) so the same logical token can have distinct registrations in different buckets — e.g., `imports: [CacheModule.forRoot({ttl:60}), CacheModule.forRoot({ttl:120})]` now actually produces two reachable cache configs instead of silently dropping one. A consumer module that imports both throws `MultipleProvidersFoundError` with both candidate ids in the message; resolve only one and the ambiguity disappears. `[HttpModule, HttpModule.forRoot({base:X})]` registers both and the loader emits a diagnostic warning. `createModuleRef()` is removed — module authors use `{module: RealClass, key}` directly. New helpers `defineDynamicModule()` and `stableHash()` are exported from the root.
|
|
10
|
+
|
|
11
|
+
Pre-fix this swallowed configuration silently in two places: same-class `forRoot` calls collided at `processedModules.has(class)`, and synthetic-class-per-call patterns (the old `HttpModule`) collided at the container's `if (!has(token))` provider guard. Both guards are gone.
|
|
12
|
+
|
|
9
13
|
- **`REQUEST_CONTEXT` injectable.** A request-scoped primitive carrying a stable `id` (mirrored from inbound `x-request-id` if present, else `crypto.randomUUID()`), `receivedAt`, the raw `Request`, the Hono `Context`, and a typed `set/get/has` bag for cross-cutting metadata. Seeded by `RouteManager` into each per-request child container; resolves through `@Inject(REQUEST_CONTEXT)` from any request-scoped service. No `AsyncLocalStorage` — edge-runtime contract intact (verified live under workerd via `pnpm test:workers`).
|
|
10
14
|
|
|
11
15
|
```ts
|
|
@@ -21,6 +25,11 @@ A sanctioned per-request injectable lands as a framework primitive, and the meta
|
|
|
21
25
|
}
|
|
22
26
|
```
|
|
23
27
|
|
|
28
|
+
### Breaking
|
|
29
|
+
|
|
30
|
+
- **`createModuleRef()` removed.** The synthetic-class-per-`forRoot()` workaround is gone; modules use `{ module: RealClass, key }` instead. First-party modules (`HttpModule`, `CacheModule`, `ConfigModule`, `ThrottlerModule`, `CorsModule`) are migrated. Sibling consumers (`@velajs/cloudflare`, `@velajs/crud`) need to switch from `createModuleRef('${name}_${key}')` to `key: ...` on the DynamicModule.
|
|
31
|
+
- **Container provider storage shape changed.** `Container.providers` is now `Map<moduleId, Map<Token, Registration>>` instead of flat. `providerOrigin` is removed (origin is now `declaringModuleId` on each `ProviderRegistration`). `assertVisible` is removed (its role is subsumed by lookup-or-throw in `resolve`). `Container.has(token)` is preserved (any-bucket scan); a new `Container.hasInScope(token, moduleId)` is the strict variant. `resolveAll(token)` is now a real walk across reachable buckets, not a stub. New error: `MultipleProvidersFoundError` when an imports walk yields >1 candidate. New constant: `ROOT_MODULE_ID = '__root__'` (sentinel bucket for bootstrap primitives and sandbox registrations).
|
|
32
|
+
|
|
24
33
|
### Internal cleanup
|
|
25
34
|
|
|
26
35
|
- **Metadata stacking + funnel coverage** (audit #8 follow-up). New `metadata-stacking.test.ts` asserts `appendCustomHandlerMeta` is order-deterministic, `Reflect.defineMetadata` round-trips through `MetadataRegistry`'s typed slots, `MetadataRegistry.reset()` clears `classMeta`/`handlerMeta`, and class+handler `@SetMetadata` on the same key remain independent.
|
package/README.md
CHANGED
|
@@ -104,6 +104,45 @@ Configurable modules use `forRoot` (sync) and `forRootAsync` (DI-resolved):
|
|
|
104
104
|
class AppModule {}
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Identity model
|
|
108
|
+
|
|
109
|
+
Each `DynamicModule` has an optional `key?: string` that discriminates one instance from another. First-party modules derive `key: stableHash(options)` automatically inside `forRoot` — so the same options always dedup, and distinct options register as distinct instances:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Same options → dedup (one CacheModule instance, ttl: 60)
|
|
113
|
+
imports: [
|
|
114
|
+
CacheModule.forRoot({ ttl: 60 }),
|
|
115
|
+
CacheModule.forRoot({ ttl: 60 }),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
// Different options → two distinct instances coexist
|
|
119
|
+
imports: [
|
|
120
|
+
CacheModule.forRoot({ ttl: 60 }),
|
|
121
|
+
CacheModule.forRoot({ ttl: 120 }),
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
When a consumer module imports two instances both exporting the same logical token, the resolver throws `MultipleProvidersFoundError` with both candidate ids — resolve the ambiguity by importing only one, or use a per-instance accessor exposed by the module. Most apps with a single instance never hit this.
|
|
126
|
+
|
|
127
|
+
Custom modules can use the same pattern via the public helpers:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { defineDynamicModule, stableHash } from '@velajs/vela';
|
|
131
|
+
|
|
132
|
+
class MyModule {
|
|
133
|
+
static forRoot(options: MyOptions): DynamicModule {
|
|
134
|
+
return defineDynamicModule({
|
|
135
|
+
module: MyModule,
|
|
136
|
+
key: stableHash(options), // or pass an explicit key
|
|
137
|
+
providers: [/* ... */],
|
|
138
|
+
exports: [/* ... */],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`forRootAsync` callers should pass `key` explicitly when the same module needs multiple async instances — factories aren't structurally hashable.
|
|
145
|
+
|
|
107
146
|
## Companion packages
|
|
108
147
|
|
|
109
148
|
| Package | Purpose |
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stableHash } from "../module/stable-hash.js";
|
|
1
2
|
import { APP_INTERCEPTOR } from "../pipeline/tokens.js";
|
|
2
3
|
import { CacheInterceptor } from "./cache.interceptor.js";
|
|
3
4
|
import { CacheService } from "./cache.service.js";
|
|
@@ -27,6 +28,7 @@ export class CacheModule {
|
|
|
27
28
|
}
|
|
28
29
|
return {
|
|
29
30
|
module: CacheModule,
|
|
31
|
+
key: stableHash(options),
|
|
30
32
|
providers,
|
|
31
33
|
exports: [
|
|
32
34
|
CACHE_MANAGER,
|
|
@@ -61,6 +63,10 @@ export class CacheModule {
|
|
|
61
63
|
}
|
|
62
64
|
return {
|
|
63
65
|
module: CacheModule,
|
|
66
|
+
key: options.key ?? stableHash({
|
|
67
|
+
inject: options.inject,
|
|
68
|
+
useFactory: options.useFactory
|
|
69
|
+
}),
|
|
64
70
|
imports: options.imports ?? [],
|
|
65
71
|
providers,
|
|
66
72
|
exports: [
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AsyncModuleOptions, DynamicModule } from '../module/types';
|
|
2
2
|
import type { ConfigModuleOptions } from './config.types';
|
|
3
3
|
export declare class ConfigModule {
|
|
4
|
-
static forRoot<T extends Record<string, unknown>>(options: ConfigModuleOptions<T>
|
|
4
|
+
static forRoot<T extends Record<string, unknown>>(options: ConfigModuleOptions<T> & {
|
|
5
|
+
key?: string;
|
|
6
|
+
}): DynamicModule;
|
|
5
7
|
static forRootAsync<T extends Record<string, unknown>>(options: AsyncModuleOptions<ConfigModuleOptions<T>> & {
|
|
6
8
|
isGlobal?: boolean;
|
|
9
|
+
key?: string;
|
|
7
10
|
}): DynamicModule;
|
|
8
11
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stableHash } from "../module/stable-hash.js";
|
|
1
2
|
import { ConfigService } from "./config.service.js";
|
|
2
3
|
import { CONFIG_OPTIONS } from "./config.tokens.js";
|
|
3
4
|
export class ConfigModule {
|
|
@@ -5,6 +6,7 @@ export class ConfigModule {
|
|
|
5
6
|
const config = options.validate ? options.validate(options.config) : options.config;
|
|
6
7
|
return {
|
|
7
8
|
module: ConfigModule,
|
|
9
|
+
key: options.key ?? stableHash(options.config),
|
|
8
10
|
providers: [
|
|
9
11
|
{
|
|
10
12
|
provide: CONFIG_OPTIONS,
|
|
@@ -24,6 +26,10 @@ export class ConfigModule {
|
|
|
24
26
|
static forRootAsync(options) {
|
|
25
27
|
return {
|
|
26
28
|
module: ConfigModule,
|
|
29
|
+
key: options.key ?? stableHash({
|
|
30
|
+
inject: options.inject,
|
|
31
|
+
useFactory: options.useFactory
|
|
32
|
+
}),
|
|
27
33
|
imports: options.imports ?? [],
|
|
28
34
|
providers: [
|
|
29
35
|
{
|
|
@@ -1,25 +1,55 @@
|
|
|
1
1
|
import { Scope } from '../constants';
|
|
2
2
|
import type { ContainerOptions, Diagnostics, ModuleScope, ProviderOptions, Token, Type } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Per-module provider buckets. Each module instance owns its providers under
|
|
5
|
+
* its `moduleId`; the same logical token can have distinct registrations in
|
|
6
|
+
* different buckets, supporting multi-instance dynamic modules.
|
|
7
|
+
*
|
|
8
|
+
* The `__root__` bucket holds bootstrap-time framework primitives (Container,
|
|
9
|
+
* ModuleRef, REQUEST_CONTEXT, etc.) and any `register()` call that doesn't
|
|
10
|
+
* supply a `declaringModuleId`.
|
|
11
|
+
*/
|
|
3
12
|
export declare class Container {
|
|
4
13
|
private providers;
|
|
14
|
+
private exporterIndex;
|
|
5
15
|
private resolutionStack;
|
|
6
16
|
private requestInstances;
|
|
7
17
|
private scopes;
|
|
8
18
|
private globals;
|
|
9
|
-
private providerOrigin;
|
|
10
19
|
private diagnostics;
|
|
11
20
|
constructor(options?: ContainerOptions);
|
|
12
21
|
register<T>(provider: Type<T> | ProviderOptions<T>, declaringModuleId?: string): this;
|
|
13
22
|
private registerClass;
|
|
14
23
|
private registerOptions;
|
|
15
|
-
private
|
|
24
|
+
private writeRegistration;
|
|
16
25
|
registerScope(scope: ModuleScope): void;
|
|
17
26
|
markGlobalToken(token: Token): void;
|
|
18
27
|
setRequestInstance(token: Token, value: unknown): void;
|
|
19
28
|
getDiagnostics(): Diagnostics;
|
|
20
29
|
resolve<T>(token: Token<T>, requestingModuleId?: string): T;
|
|
21
30
|
resolveAll<T>(token: Token<T>, requestingModuleId?: string): T[];
|
|
31
|
+
/**
|
|
32
|
+
* Find a single reachable registration for a token, applying the visibility
|
|
33
|
+
* walk: requester's bucket → globals → requester's importedModules
|
|
34
|
+
* (transitively, following re-exports).
|
|
35
|
+
*
|
|
36
|
+
* Returns `undefined` if no candidate exists. Throws
|
|
37
|
+
* `MultipleProvidersFoundError` if the imports walk yields more than one.
|
|
38
|
+
*/
|
|
39
|
+
private findRegistration;
|
|
40
|
+
/** Typed bucket lookup — single seam where the Map<Token, Registration> erases T. */
|
|
41
|
+
private lookupInBucket;
|
|
42
|
+
/**
|
|
43
|
+
* Walk a scope's imports (and their imports' imports, …) collecting every
|
|
44
|
+
* registration reachable through re-export chains. A child only contributes
|
|
45
|
+
* if it explicitly exports the token — non-exported providers stay private.
|
|
46
|
+
*/
|
|
47
|
+
private collectFromImports;
|
|
48
|
+
private findAllRegistrations;
|
|
49
|
+
/** True if any bucket has a registration for `token`. Sandbox-friendly. */
|
|
22
50
|
has(token: Token): boolean;
|
|
51
|
+
/** True if the specified module's bucket has a registration for `token`. */
|
|
52
|
+
hasInScope(token: Token, moduleId: string): boolean;
|
|
23
53
|
getProviderScope(token: Token): Scope | undefined;
|
|
24
54
|
getTokens(): Token[];
|
|
25
55
|
/**
|
|
@@ -35,7 +65,5 @@ export declare class Container {
|
|
|
35
65
|
private resolveFactory;
|
|
36
66
|
resolveAsync<T>(token: Token<T>, requestingModuleId?: string): Promise<T>;
|
|
37
67
|
private createLazyProxy;
|
|
38
|
-
private assertVisible;
|
|
39
|
-
private isExportedFromImports;
|
|
40
68
|
private tokenToString;
|
|
41
69
|
}
|