@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +52 -0
  3. package/dist/cache/cache.module.d.ts +1 -0
  4. package/dist/cache/cache.module.js +6 -0
  5. package/dist/config/config.module.d.ts +4 -1
  6. package/dist/config/config.module.js +6 -0
  7. package/dist/container/container.d.ts +32 -4
  8. package/dist/container/container.js +252 -103
  9. package/dist/container/decorators.d.ts +8 -1
  10. package/dist/container/decorators.js +7 -4
  11. package/dist/container/index.d.ts +1 -1
  12. package/dist/container/index.js +1 -1
  13. package/dist/container/types.d.ts +13 -0
  14. package/dist/container/types.js +10 -0
  15. package/dist/cors/cors.module.d.ts +3 -1
  16. package/dist/cors/cors.module.js +2 -0
  17. package/dist/fetch/fetch.module.d.ts +3 -1
  18. package/dist/fetch/fetch.module.js +8 -3
  19. package/dist/http/argument-resolver.d.ts +10 -0
  20. package/dist/http/argument-resolver.js +89 -0
  21. package/dist/http/handler-executor.d.ts +20 -0
  22. package/dist/http/handler-executor.js +108 -0
  23. package/dist/http/instantiate.d.ts +4 -0
  24. package/dist/http/instantiate.js +18 -0
  25. package/dist/http/response-mapper.d.ts +8 -0
  26. package/dist/http/response-mapper.js +42 -0
  27. package/dist/http/route.manager.d.ts +1 -9
  28. package/dist/http/route.manager.js +23 -240
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +2 -2
  31. package/dist/module/decorators.d.ts +6 -2
  32. package/dist/module/decorators.js +7 -6
  33. package/dist/module/index.d.ts +2 -1
  34. package/dist/module/index.js +2 -1
  35. package/dist/module/module-loader.d.ts +6 -1
  36. package/dist/module/module-loader.js +127 -47
  37. package/dist/module/stable-hash.d.ts +15 -0
  38. package/dist/module/stable-hash.js +52 -0
  39. package/dist/registry/types.d.ts +11 -0
  40. package/dist/throttler/throttler.module.d.ts +6 -2
  41. package/dist/throttler/throttler.module.js +6 -0
  42. 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, and the metadata-store unification finally has its regression tests.
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,9 +25,17 @@ 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.
36
+ - **`NestModule.configure()` regression coverage** (audit #4 follow-up). New `configure-resolution.test.ts` asserts configure-time DI works (modules can constructor-inject providers from their own scope), synchronous errors thrown from `configure()` propagate, and unresolvable constructor deps fail loudly rather than silently skipping middleware setup.
37
+ - **Edge-safe contract documented** (audit #7 follow-up). README now states the contract explicitly: the main export is edge-safe and audited in CI by `src/__tests__/edge-runtime-audit.test.ts`; `@velajs/vela/schedule-node` is the one opt-in Node/Bun carve-out.
38
+ - **`RouteManager` split into focused units** (audit #9 follow-up). New files `argument-resolver.ts`, `handler-executor.ts`, `response-mapper.ts`, and `instantiate.ts` carry parameter extraction, the per-request orchestration closure, response/redirect mapping, and the container-aware factory. `RouteManager` is now focused on Hono route registration and path composition. Internal-only refactor — no public API change, no behavior change.
27
39
  - **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
28
40
  - **`Container.setRequestInstance(token, value)`** — public method to pre-seed the per-request cache. Used by `RouteManager` to populate `REQUEST_CONTEXT` before any handler resolution runs.
29
41
 
package/README.md CHANGED
@@ -73,6 +73,19 @@ Vela runs on any runtime that supports the Web Standards API:
73
73
 
74
74
  No Node.js-specific APIs (`node:fs`, `Buffer`, `process`) are used.
75
75
 
76
+ ### Edge-safe contract
77
+
78
+ The **main export** (`@velajs/vela`) is edge-safe by contract — no `node:*` imports, no `Buffer`, no `process`, no `setInterval`, no `Bun.serve`. This is enforced in CI by [`src/__tests__/edge-runtime-audit.test.ts`](src/__tests__/edge-runtime-audit.test.ts), which fails the build if any file under `src/` references a forbidden API.
79
+
80
+ One subpath, **`@velajs/vela/schedule-node`**, is an opt-in Node/Bun adapter for `setInterval`-based job execution. It uses runtime-specific APIs by design and is **excluded from the edge-runtime audit**. Edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge) should not import it — use platform cron triggers instead (e.g., `@velajs/cloudflare` ≥ 0.2.0 dispatches `@Cron` jobs via the Workers `scheduled()` handler).
81
+
82
+ ```ts
83
+ // Node / Bun only — opt-in
84
+ import { ScheduleNodeModule } from '@velajs/vela/schedule-node';
85
+ ```
86
+
87
+ The other subpaths (`@velajs/vela/internal`, `@velajs/vela/streaming`) follow the main export's edge-safe contract.
88
+
76
89
  ## Dynamic modules
77
90
 
78
91
  Configurable modules use `forRoot` (sync) and `forRootAsync` (DI-resolved):
@@ -91,6 +104,45 @@ Configurable modules use `forRoot` (sync) and `forRootAsync` (DI-resolved):
91
104
  class AppModule {}
92
105
  ```
93
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
+
94
146
  ## Companion packages
95
147
 
96
148
  | Package | Purpose |
@@ -4,5 +4,6 @@ export declare class CacheModule {
4
4
  static forRoot(options?: CacheModuleOptions): DynamicModule;
5
5
  static forRootAsync(options: AsyncModuleOptions<CacheModuleOptions> & {
6
6
  isGlobal?: boolean;
7
+ key?: string;
7
8
  }): DynamicModule;
8
9
  }
@@ -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>): DynamicModule;
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 recordOrigin;
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
  }