@velajs/vela 1.3.0 → 1.4.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 CHANGED
@@ -2,6 +2,33 @@
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.
6
+
7
+ ### New
8
+
9
+ - **`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
+
11
+ ```ts
12
+ import { Inject, Injectable, Scope, REQUEST_CONTEXT } from '@velajs/vela';
13
+ import type { RequestContext } from '@velajs/vela';
14
+
15
+ @Injectable({ scope: Scope.REQUEST })
16
+ class TenantResolver {
17
+ constructor(@Inject(REQUEST_CONTEXT) private readonly ctx: RequestContext) {}
18
+ resolve() {
19
+ return this.ctx.hono.req.header('x-tenant') ?? 'default';
20
+ }
21
+ }
22
+ ```
23
+
24
+ ### Internal cleanup
25
+
26
+ - **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.
27
+ - **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
28
+ - **`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
+
30
+ ## 1.3.0
31
+
5
32
  Module boundaries are enforced. NestJS-shape: a service cannot resolve dependencies from a module it didn't import. Bootstrap is consolidated into a single primitive, discovery failures are diagnostically routed, and a generic plugin composer is included.
6
33
 
7
34
  ### New
@@ -15,6 +15,7 @@ export declare class Container {
15
15
  private recordOrigin;
16
16
  registerScope(scope: ModuleScope): void;
17
17
  markGlobalToken(token: Token): void;
18
+ setRequestInstance(token: Token, value: unknown): void;
18
19
  getDiagnostics(): Diagnostics;
19
20
  resolve<T>(token: Token<T>, requestingModuleId?: string): T;
20
21
  resolveAll<T>(token: Token<T>, requestingModuleId?: string): T[];
@@ -86,6 +86,14 @@ export class Container {
86
86
  markGlobalToken(token) {
87
87
  this.globals.add(token);
88
88
  }
89
+ // Pre-seed the per-request cache. Only meaningful on a child container
90
+ // produced by createChild() — the root's requestInstances map is unused.
91
+ // Used by RouteManager to populate framework-provided request-scope
92
+ // values (REQUEST_CONTEXT) before any handler resolution runs, so the
93
+ // provider's factory never fires on the request path.
94
+ setRequestInstance(token, value) {
95
+ this.requestInstances.set(token, value);
96
+ }
89
97
  getDiagnostics() {
90
98
  return this.diagnostics;
91
99
  }
@@ -1,5 +1,7 @@
1
+ import { Scope } from "../constants.js";
1
2
  import { Container } from "../container/container.js";
2
3
  import { ModuleRef } from "../container/module-ref.js";
4
+ import { REQUEST_CONTEXT } from "../http/request-context.js";
3
5
  import { RouteManager } from "../http/route.manager.js";
4
6
  import { ModuleLoader } from "../module/module-loader.js";
5
7
  import { bindAppProviders } from "../pipeline/app-providers.js";
@@ -39,6 +41,18 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
39
41
  ]){
40
42
  container.markGlobalToken(t);
41
43
  }
44
+ // REQUEST_CONTEXT is seeded into each request-scoped child by RouteManager
45
+ // before any handler resolves it (see route.manager.ts:getRequestContainer).
46
+ // The factory throws so misuse outside the request path surfaces immediately
47
+ // instead of materializing a phantom context.
48
+ container.register({
49
+ provide: REQUEST_CONTEXT,
50
+ scope: Scope.REQUEST,
51
+ useFactory: ()=>{
52
+ throw new Error('REQUEST_CONTEXT can only be resolved inside a request — ' + 'it is seeded by RouteManager when the request enters the pipeline.');
53
+ }
54
+ });
55
+ container.markGlobalToken(REQUEST_CONTEXT);
42
56
  const routeManager = new RouteManager(container, options);
43
57
  ComponentManager.init(container);
44
58
  const loader = new ModuleLoader(container, routeManager);
@@ -0,0 +1,13 @@
1
+ import type { Context } from 'hono';
2
+ import { InjectionToken } from '../container/types';
3
+ export interface RequestContext {
4
+ readonly id: string;
5
+ readonly receivedAt: Date;
6
+ readonly request: Request;
7
+ readonly hono: Context;
8
+ set<T>(key: string | symbol, value: T): void;
9
+ get<T>(key: string | symbol): T | undefined;
10
+ has(key: string | symbol): boolean;
11
+ }
12
+ export declare const REQUEST_CONTEXT: InjectionToken<RequestContext>;
13
+ export declare function createRequestContext(c: Context): RequestContext;
@@ -0,0 +1,22 @@
1
+ import { InjectionToken } from "../container/types.js";
2
+ export const REQUEST_CONTEXT = new InjectionToken('vela.RequestContext');
3
+ export function createRequestContext(c) {
4
+ const bag = new Map();
5
+ const inboundId = c.req.raw.headers.get('x-request-id');
6
+ const id = inboundId && inboundId.length > 0 ? inboundId : crypto.randomUUID();
7
+ return {
8
+ id,
9
+ receivedAt: new Date(),
10
+ request: c.req.raw,
11
+ hono: c,
12
+ set (key, value) {
13
+ bag.set(key, value);
14
+ },
15
+ get (key) {
16
+ return bag.get(key);
17
+ },
18
+ has (key) {
19
+ return bag.has(key);
20
+ }
21
+ };
22
+ }
@@ -4,6 +4,7 @@ import { HttpMethod, ParamType } from "../constants.js";
4
4
  import { getMetadata } from "../metadata.js";
5
5
  import { joinPaths } from "../registry/paths.js";
6
6
  import { buildExecutionContext } from "./execution-context.js";
7
+ import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
7
8
  import { ForbiddenException, HttpException } from "../errors/http-exception.js";
8
9
  import { ComponentManager } from "../pipeline/component.manager.js";
9
10
  import { shouldFilterCatch } from "../pipeline/decorators.js";
@@ -206,6 +207,7 @@ export class RouteManager {
206
207
  return existing;
207
208
  }
208
209
  const child = this.container.createChild();
210
+ child.setRequestInstance(REQUEST_CONTEXT, createRequestContext(c));
209
211
  c.set('container', child);
210
212
  return child;
211
213
  }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
10
10
  export type { Type, Token, InjectableOptions, ProviderOptions, ModuleScope, ContainerOptions, Diagnostics, } from './container/index';
11
11
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
12
12
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, } from './http/index';
13
+ export { REQUEST_CONTEXT } from './http/request-context';
14
+ export type { RequestContext } from './http/request-context';
13
15
  export { Logger, LogLevel } from './services/index';
14
16
  export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
15
17
  export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
12
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
13
13
  // HTTP Decorators
14
14
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators } from "./http/index.js";
15
+ // Request-scoped context primitive
16
+ export { REQUEST_CONTEXT } from "./http/request-context.js";
15
17
  // Services
16
18
  export { Logger, LogLevel } from "./services/index.js";
17
19
  // Config
@@ -265,6 +265,5 @@ export class MetadataRegistry {
265
265
  this.handlerComponents[type].clear();
266
266
  }
267
267
  this.globalComponents = emptyComponentStore();
268
- // reflectMeta is a WeakMap — entries die with their targets.
269
268
  }
270
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",