@velajs/vela 1.5.1 → 1.8.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
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.0 (2026-05-14)
4
+
5
+ ### New
6
+
7
+ - **`OnFirstRequest` lifecycle hook.** Fires exactly once on the first incoming HTTP request, AFTER user-supplied middleware (so runtime adapters like `@velajs/cloudflare`'s binding-init can run first) and BEFORE route handlers. Bridges module-load and request-time semantics — the canonical seam for state that depends on values only available at request time (Cloudflare D1/KV/R2 bindings, Deno Deploy env, etc.). Auto-fired by a global middleware vela installs after user middleware; non-HTTP consumers can trigger it manually via `app.callOnFirstRequest()`. Concurrency-safe: vela memoizes the promise so parallel first requests share a single fire.
8
+
9
+ ### Notes
10
+
11
+ - Edge-clean addition — no `node:*`, no `Buffer`, no `setInterval`; the hook is just `() => void | Promise<void>` wired into Hono's middleware chain.
12
+ - Backwards-compatible. Existing apps unaffected: the new middleware only fires hooks if any registered instance implements `OnFirstRequest`, and the implementation is a no-op otherwise.
13
+
14
+ ## [1.6.0]
15
+
16
+ The exception-filter chain now reaches into attached middlewares for full NestJS parity, and a new `createLazyParamDecorator` helper closes the parameter-decorator-vs-guard ordering hazard at the public-API surface.
17
+
18
+ ### Added
19
+
20
+ - **`createLazyParamDecorator((data, ctx) => T)`.** Custom parameter decorators whose factory runs the *first time the handler reads a property on the resolved value* — not during argument extraction. Vela's argument resolver runs before guards by design (`extract args → guards → handler`), which means a `createParamDecorator` factory that depends on guard-populated state observes an empty slot. The lazy variant returns a `Proxy` whose traps invoke the factory on demand; the `get` trap short-circuits `prop === 'then'` so `await value` does not consider the proxy a thenable and therefore does not trigger eager resolution. Method results are auto-bound to the resolved real target so detached calls keep `this`; `ownKeys` + `getOwnPropertyDescriptor` are implemented so `JSON.stringify(value)` works after one access. Exported from the root barrel and from `@velajs/vela/internal` via the same surface as `createParamDecorator`. Documented in README under *Custom parameter decorators with deferred resolution*.
21
+
22
+ ### Changed
23
+
24
+ - **Exception-filter chain now catches errors thrown inside vela-attached middlewares.** Previously the `APP_FILTER` / `@UseFilters` chain only handled errors thrown from `@Controller` handler methods; errors from middlewares (global, `MiddlewareConsumer.apply(...).forRoutes(...)`, and per-route attached) bypassed the chain and surfaced as Hono's outer 500. They now flow through the same filter resolution as handler exceptions, with a synthesized `ExecutionContext` whose `getClass()` returns the `VelaMiddlewareHost` marker and whose `getHandler()` returns the `Symbol.for('vela.middleware')` sentinel — `getType()` stays `'http'` for NestJS parity. Only global filters apply at the middleware boundary (per-handler `@UseFilters` requires a controller call frame); for thrown `HttpException`s with no catching filter, the chain renders the exception's own response/status (matching handler-thrown semantics). Non-`HttpException` throws with no catching filter re-throw to preserve the existing default-500 path. Non-throwing middleware paths (returning a `Response`, calling `await next()`, resolving a promise) are byte-identical to before.
25
+
3
26
  ## Unreleased
4
27
 
5
28
  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.
package/README.md CHANGED
@@ -143,6 +143,51 @@ class MyModule {
143
143
 
144
144
  `forRootAsync` callers should pass `key` explicitly when the same module needs multiple async instances — factories aren't structurally hashable.
145
145
 
146
+ ## Custom parameter decorators with deferred resolution
147
+
148
+ Vela's argument resolver runs **before** guards (`extract args → guards → handler`). A custom parameter decorator built with `createParamDecorator` therefore observes any state populated by a guard as still empty — its factory has already fired by the time the guard runs.
149
+
150
+ When a parameter's value depends on guard output (a `REQUEST_CONTEXT`-stored user, a tenant resolved from a JWT, etc.), use `createLazyParamDecorator` instead. The factory does not run during argument extraction; it runs the first time the handler reads a property on the resolved value:
151
+
152
+ ```ts
153
+ import {
154
+ createLazyParamDecorator,
155
+ Inject,
156
+ Injectable,
157
+ REQUEST_CONTEXT,
158
+ Scope,
159
+ UseGuards,
160
+ } from '@velajs/vela';
161
+ import type { CanActivate, ExecutionContext, RequestContext } from '@velajs/vela';
162
+
163
+ const USER_KEY = Symbol.for('app.user');
164
+
165
+ @Injectable({ scope: Scope.REQUEST })
166
+ class AuthGuard implements CanActivate {
167
+ constructor(@Inject(REQUEST_CONTEXT) private readonly ctx: RequestContext) {}
168
+ canActivate(_e: ExecutionContext): boolean {
169
+ this.ctx.set(USER_KEY, { id: 'u-1', name: 'ada' }); // populated here
170
+ return true;
171
+ }
172
+ }
173
+
174
+ const CurrentUser = createLazyParamDecorator((_data, ctx: ExecutionContext) => {
175
+ const reqCtx = ctx
176
+ .getContext()
177
+ .get('container')
178
+ .resolve<RequestContext>(REQUEST_CONTEXT);
179
+ return reqCtx.get(USER_KEY);
180
+ });
181
+
182
+ @UseGuards(AuthGuard)
183
+ @Get('/me')
184
+ me(@CurrentUser() user: { id: string; name: string }) {
185
+ return { id: user.id }; // factory runs here, AFTER AuthGuard
186
+ }
187
+ ```
188
+
189
+ The proxy short-circuits `then` on its `get` trap so `await value` returns the proxy itself rather than triggering eager resolution. Method results are auto-bound to the resolved real target, so detached method calls keep `this`. `JSON.stringify(value)` works after one access (the proxy implements `ownKeys` + `getOwnPropertyDescriptor`).
190
+
146
191
  ## Companion packages
147
192
 
148
193
  | Package | Purpose |
@@ -9,6 +9,7 @@ export declare class VelaApplication {
9
9
  private readonly routeManager;
10
10
  private instances;
11
11
  private honoApp;
12
+ private firstRequestPromise;
12
13
  constructor(container: Container, routeManager: RouteManager);
13
14
  /** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */
14
15
  initRoutes(): Promise<void>;
@@ -36,5 +37,14 @@ export declare class VelaApplication {
36
37
  mountOpenApi(options: MountOpenApiOptions): this;
37
38
  callOnModuleInit(): Promise<void>;
38
39
  callOnApplicationBootstrap(): Promise<void>;
40
+ /**
41
+ * Fire `OnFirstRequest` hooks exactly once across the application's
42
+ * lifetime. Auto-invoked by a global middleware on the first HTTP request;
43
+ * also safe to call manually from non-HTTP entry points (CLI, tests).
44
+ *
45
+ * Concurrent calls share the same in-flight promise — vela guarantees
46
+ * each instance's `onFirstRequest()` runs at most once.
47
+ */
48
+ callOnFirstRequest(): Promise<void>;
39
49
  close(signal?: string): Promise<void>;
40
50
  }
@@ -1,10 +1,13 @@
1
- import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
1
+ import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnFirstRequest, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
2
2
  import { renderScalarUi } from "./openapi/scalar-ui.js";
3
3
  export class VelaApplication {
4
4
  container;
5
5
  routeManager;
6
6
  instances = [];
7
7
  honoApp = null;
8
+ // Memoizes the first-request hook fire. Concurrent first requests share
9
+ // this promise so the hook runs exactly once.
10
+ firstRequestPromise;
8
11
  constructor(container, routeManager){
9
12
  this.container = container;
10
13
  this.routeManager = routeManager;
@@ -89,6 +92,25 @@ export class VelaApplication {
89
92
  }
90
93
  }
91
94
  }
95
+ /**
96
+ * Fire `OnFirstRequest` hooks exactly once across the application's
97
+ * lifetime. Auto-invoked by a global middleware on the first HTTP request;
98
+ * also safe to call manually from non-HTTP entry points (CLI, tests).
99
+ *
100
+ * Concurrent calls share the same in-flight promise — vela guarantees
101
+ * each instance's `onFirstRequest()` runs at most once.
102
+ */ callOnFirstRequest() {
103
+ if (!this.firstRequestPromise) {
104
+ this.firstRequestPromise = (async ()=>{
105
+ for (const instance of this.instances){
106
+ if (hasOnFirstRequest(instance)) {
107
+ await instance.onFirstRequest();
108
+ }
109
+ }
110
+ })();
111
+ }
112
+ return this.firstRequestPromise;
113
+ }
92
114
  async close(signal) {
93
115
  const reversed = [
94
116
  ...this.instances
package/dist/factory.js CHANGED
@@ -6,6 +6,18 @@ export const VelaFactory = {
6
6
  const app = new VelaApplication(container, routeManager);
7
7
  const instances = await loader.resolveAllInstances();
8
8
  app.setInstances(instances);
9
+ // Register the first-request middleware AFTER user-supplied middleware
10
+ // (registered by bootstrap from `options.middleware`). Ordering matters:
11
+ // runtime adapters like `@velajs/cloudflare` install binding-init
12
+ // middleware that must run BEFORE OnFirstRequest hooks read those
13
+ // bindings. Vela's middleware is appended last → fires after all user
14
+ // middleware, before route handlers (which are registered separately).
15
+ routeManager.useGlobalMiddleware({
16
+ use: async (_c, next)=>{
17
+ await app.callOnFirstRequest();
18
+ await next();
19
+ }
20
+ });
9
21
  await app.callOnModuleInit();
10
22
  await app.callOnApplicationBootstrap();
11
23
  // Build routes (async — supports CRUD integration with dynamic imports)
@@ -2,3 +2,7 @@ import type { Context } from 'hono';
2
2
  import type { Type } from '../container/types';
3
3
  import type { ExecutionContext } from '../pipeline/types';
4
4
  export declare function buildExecutionContext(c: Context, controller: Type, handlerName: string | symbol): ExecutionContext;
5
+ export declare class VelaMiddlewareHost {
6
+ }
7
+ export declare const VELA_MIDDLEWARE_HANDLER: unique symbol;
8
+ export declare function buildMiddlewareExecutionContext(c: Context): ExecutionContext;
@@ -13,3 +13,20 @@ export function buildExecutionContext(c, controller, handlerName) {
13
13
  })
14
14
  };
15
15
  }
16
+ // Sentinel marker exposed via `ExecutionContext.getClass()` when the host is a
17
+ // vela-attached middleware rather than a controller handler. Filters that
18
+ // branch on the calling class (rare) can detect the middleware origin without
19
+ // introducing a separate `getType()` value — `getType()` stays `'http'` for
20
+ // NestJS parity.
21
+ export class VelaMiddlewareHost {
22
+ }
23
+ // Synthesizes an ExecutionContext for an exception thrown inside a
24
+ // vela-attached middleware. There is no controller class or handler method
25
+ // on the call stack at that point, so `getClass()` returns the
26
+ // `VelaMiddlewareHost` marker and `getHandler()` returns the
27
+ // `vela.middleware` symbol — both stable, comparable values that filter
28
+ // authors can pattern-match against if needed.
29
+ export const VELA_MIDDLEWARE_HANDLER = Symbol.for('vela.middleware');
30
+ export function buildMiddlewareExecutionContext(c) {
31
+ return buildExecutionContext(c, VelaMiddlewareHost, VELA_MIDDLEWARE_HANDLER);
32
+ }
@@ -3,4 +3,5 @@ export type { RouteManagerOptions } from './route.manager';
3
3
  export { MiddlewareBuilder } from '../module/middleware';
4
4
  export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
5
5
  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, isController, } from './decorators';
6
+ export { createLazyParamDecorator } from './lazy-param.decorator';
6
7
  export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
@@ -1,3 +1,4 @@
1
1
  export { RouteManager } from "./route.manager.js";
2
2
  export { MiddlewareBuilder } from "../module/middleware.js";
3
3
  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, isController } from "./decorators.js";
4
+ export { createLazyParamDecorator } from "./lazy-param.decorator.js";
@@ -0,0 +1,37 @@
1
+ import type { PipeType } from '../registry/types';
2
+ import type { ExecutionContext } from '../pipeline/types';
3
+ /**
4
+ * Factory for parameter decorators whose value materializes lazily — the
5
+ * factory does not run during argument extraction; it runs the first time
6
+ * the handler reads a property on the resolved value.
7
+ *
8
+ * Why this exists: vela's argument resolver runs *before* guards
9
+ * (handler-executor order: extract args → guards → handler). A custom
10
+ * `createParamDecorator` factory that depends on guard-populated state
11
+ * (e.g. a value a guard places into `REQUEST_CONTEXT`) therefore observes
12
+ * an empty slot. `createLazyParamDecorator` defers factory execution to
13
+ * first property access on the proxied result, by which point guards have
14
+ * run and the slot is populated.
15
+ *
16
+ * The proxy explicitly short-circuits `prop === 'then'` so `await value`
17
+ * does not consider the proxy a thenable, which would otherwise trigger
18
+ * eager resolution. This single invariant is what makes the helper safe to
19
+ * pass through `async` boundaries without surprise.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const CurrentUser = createLazyParamDecorator(
24
+ * (_data: unknown, ctx: ExecutionContext) => {
25
+ * const reqCtx = ctx.getContext().get('container').resolve(REQUEST_CONTEXT);
26
+ * return reqCtx.get('user'); // populated by AuthGuard
27
+ * },
28
+ * );
29
+ *
30
+ * @UseGuards(AuthGuard)
31
+ * @Get('/me')
32
+ * me(@CurrentUser() user: User) {
33
+ * return { id: user.id }; // factory runs here, after AuthGuard
34
+ * }
35
+ * ```
36
+ */
37
+ export declare function createLazyParamDecorator<TData = unknown>(factory: (data: TData, ctx: ExecutionContext) => unknown): (data?: TData, ...pipes: PipeType[]) => ParameterDecorator;
@@ -0,0 +1,117 @@
1
+ import { MetadataRegistry } from "../registry/metadata.registry.js";
2
+ import { buildExecutionContext } from "./execution-context.js";
3
+ const CUSTOM_PARAM_TYPE = 'custom';
4
+ /**
5
+ * Factory for parameter decorators whose value materializes lazily — the
6
+ * factory does not run during argument extraction; it runs the first time
7
+ * the handler reads a property on the resolved value.
8
+ *
9
+ * Why this exists: vela's argument resolver runs *before* guards
10
+ * (handler-executor order: extract args → guards → handler). A custom
11
+ * `createParamDecorator` factory that depends on guard-populated state
12
+ * (e.g. a value a guard places into `REQUEST_CONTEXT`) therefore observes
13
+ * an empty slot. `createLazyParamDecorator` defers factory execution to
14
+ * first property access on the proxied result, by which point guards have
15
+ * run and the slot is populated.
16
+ *
17
+ * The proxy explicitly short-circuits `prop === 'then'` so `await value`
18
+ * does not consider the proxy a thenable, which would otherwise trigger
19
+ * eager resolution. This single invariant is what makes the helper safe to
20
+ * pass through `async` boundaries without surprise.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const CurrentUser = createLazyParamDecorator(
25
+ * (_data: unknown, ctx: ExecutionContext) => {
26
+ * const reqCtx = ctx.getContext().get('container').resolve(REQUEST_CONTEXT);
27
+ * return reqCtx.get('user'); // populated by AuthGuard
28
+ * },
29
+ * );
30
+ *
31
+ * @UseGuards(AuthGuard)
32
+ * @Get('/me')
33
+ * me(@CurrentUser() user: User) {
34
+ * return { id: user.id }; // factory runs here, after AuthGuard
35
+ * }
36
+ * ```
37
+ */ export function createLazyParamDecorator(factory) {
38
+ return (data, ...pipes)=>{
39
+ return (target, propertyKey, parameterIndex)=>{
40
+ if (propertyKey === undefined) {
41
+ throw new Error('Parameter decorators can only be used on method parameters');
42
+ }
43
+ MetadataRegistry.addParameter(target.constructor, propertyKey, {
44
+ index: parameterIndex,
45
+ type: CUSTOM_PARAM_TYPE,
46
+ name: undefined,
47
+ factory: (_unused, ctx)=>{
48
+ const honoCtx = ctx;
49
+ const execCtx = buildExecutionContext(honoCtx, target.constructor, propertyKey);
50
+ return createLazyProxy(()=>factory(data, execCtx));
51
+ },
52
+ ...pipes.length > 0 ? {
53
+ pipes
54
+ } : {}
55
+ });
56
+ };
57
+ };
58
+ }
59
+ // Single-shot resolution: the factory runs at most once; subsequent reads
60
+ // share the cached real value. The proxy's traps all funnel through
61
+ // `materialize` to keep that invariant in one place.
62
+ function createLazyProxy(produce) {
63
+ let resolved = false;
64
+ let value;
65
+ const materialize = ()=>{
66
+ if (!resolved) {
67
+ value = produce();
68
+ resolved = true;
69
+ }
70
+ return value;
71
+ };
72
+ // Target is an empty null-prototype object — proxies still need a
73
+ // backing target for the runtime to forward to, but using a real object
74
+ // would leak its own properties (`hasOwnProperty`, etc.) into trap
75
+ // results. `Object.create(null)` keeps the namespace clean.
76
+ const target = Object.create(null);
77
+ const handler = {
78
+ get (_t, prop, receiver) {
79
+ // Critical: do NOT report the proxy as thenable. If we let the JS
80
+ // runtime read `then` (during `await proxy`), the engine would
81
+ // observe a function-shaped value, treat the proxy as a promise,
82
+ // call `then(resolve, reject)` and trigger eager resolution before
83
+ // the consumer ever touches a real property. Returning `undefined`
84
+ // here makes `await proxy` return the proxy itself, untouched.
85
+ if (prop === 'then') return undefined;
86
+ const real = materialize();
87
+ if (real === null || real === undefined) return undefined;
88
+ const descriptor = Reflect.get(real, prop, receiver);
89
+ // Bind functions back to the real target so `this` works as the
90
+ // consumer expects on instance methods.
91
+ if (typeof descriptor === 'function') {
92
+ return descriptor.bind(real);
93
+ }
94
+ return descriptor;
95
+ },
96
+ has (_t, prop) {
97
+ const real = materialize();
98
+ return real !== null && real !== undefined && Reflect.has(real, prop);
99
+ },
100
+ ownKeys () {
101
+ const real = materialize();
102
+ if (real === null || real === undefined) return [];
103
+ return Reflect.ownKeys(real);
104
+ },
105
+ getOwnPropertyDescriptor (_t, prop) {
106
+ const real = materialize();
107
+ if (real === null || real === undefined) return undefined;
108
+ const descriptor = Reflect.getOwnPropertyDescriptor(real, prop);
109
+ // Proxy invariants require returned descriptors to be configurable
110
+ // when the underlying target lacks the property, which is always
111
+ // true for our null-prototype empty target.
112
+ if (descriptor) descriptor.configurable = true;
113
+ return descriptor;
114
+ }
115
+ };
116
+ return new Proxy(target, handler);
117
+ }
@@ -37,6 +37,8 @@ export declare class RouteManager {
37
37
  useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
38
38
  private getMiddlewarePriority;
39
39
  private getRequestContainer;
40
+ private wrapMiddlewareWithFilters;
41
+ private mapMiddlewareError;
40
42
  registerController(controller: Type): this;
41
43
  build(): Promise<Hono>;
42
44
  private registerRoute;
@@ -1,12 +1,16 @@
1
1
  import { Hono } from "hono";
2
2
  import { HttpMethod } from "../constants.js";
3
+ import { HttpException } from "../errors/http-exception.js";
3
4
  import { getMetadata } from "../metadata.js";
4
5
  import { joinPaths } from "../registry/paths.js";
5
6
  import { ArgumentResolver } from "./argument-resolver.js";
7
+ import { buildMiddlewareExecutionContext } from "./execution-context.js";
6
8
  import { HandlerExecutor } from "./handler-executor.js";
7
9
  import { instantiate, instantiateMany } from "./instantiate.js";
8
10
  import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
11
+ import { mapResponse } from "./response-mapper.js";
9
12
  import { ComponentManager } from "../pipeline/component.manager.js";
13
+ import { shouldFilterCatch } from "../pipeline/decorators.js";
10
14
  import { MetadataRegistry } from "../registry/metadata.registry.js";
11
15
  const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
12
16
  export class RouteManager {
@@ -146,6 +150,54 @@ export class RouteManager {
146
150
  c.set('container', child);
147
151
  return child;
148
152
  }
153
+ // Wraps an attached middleware so a thrown error flows through the
154
+ // exception-filter chain instead of bypassing it on the way to Hono's
155
+ // outer error handler. Mirrors the catch tail in HandlerExecutor — global
156
+ // filters are the only scope reachable here, since per-handler filters
157
+ // require a controller call frame that the middleware boundary lacks.
158
+ // Non-throwing middleware paths (returning a Response, calling `next()`,
159
+ // resolving a promise) are byte-identical to before — the wrapper only
160
+ // intercepts thrown / rejected values.
161
+ wrapMiddlewareWithFilters(handler) {
162
+ return async (c, next)=>{
163
+ try {
164
+ return await handler(c, next);
165
+ } catch (error) {
166
+ const response = await this.mapMiddlewareError(c, error);
167
+ // Force-replace any handler-set response. A middleware that throws
168
+ // *after* `await next()` has the handler's body already attached
169
+ // to `c.res`; without overwriting it, Hono would emit the
170
+ // pre-throw success response and the filter's render would be
171
+ // dropped.
172
+ c.res = response;
173
+ return response;
174
+ }
175
+ };
176
+ }
177
+ async mapMiddlewareError(c, error) {
178
+ const requestContainer = this.getRequestContainer(c);
179
+ const filters = instantiateMany(this.globalFilters, requestContainer);
180
+ const host = buildMiddlewareExecutionContext(c);
181
+ for (const filter of filters){
182
+ if (shouldFilterCatch(filter, error)) {
183
+ try {
184
+ const filtered = await filter.catch(error, host);
185
+ return mapResponse(c, filtered);
186
+ } catch {
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ if (error instanceof HttpException) {
192
+ const response = error.getResponse();
193
+ const status = error.getStatus();
194
+ return c.json(response, status);
195
+ }
196
+ // Non-HttpException with no catching filter: re-throw so Hono's
197
+ // outer error handler produces the same default-500 response it
198
+ // produced before this wrapping was introduced.
199
+ throw error;
200
+ }
149
201
  registerController(controller) {
150
202
  const prefix = MetadataRegistry.getControllerPath(controller);
151
203
  const options = MetadataRegistry.getControllerOptions(controller);
@@ -176,11 +228,11 @@ export class RouteManager {
176
228
  priority: this.getMiddlewarePriority(entry)
177
229
  })).sort((a, b)=>a.priority - b.priority || a.index - b.index);
178
230
  for (const { entry } of sortedGlobal){
179
- app.use('*', (c, next)=>{
231
+ app.use('*', this.wrapMiddlewareWithFilters((c, next)=>{
180
232
  const requestContainer = this.getRequestContainer(c);
181
233
  const resolved = instantiate(entry, requestContainer);
182
234
  return resolved.use(c, next);
183
- });
235
+ }));
184
236
  }
185
237
  const sortedConsumer = this.consumerMiddlewareDefinitions.map((def, index)=>({
186
238
  def,
@@ -190,7 +242,7 @@ export class RouteManager {
190
242
  for (const { def } of sortedConsumer){
191
243
  const matchRoute = this.compileRouteMatcher(def.routes);
192
244
  const matchExclude = this.compileRouteMatcher(def.excludes);
193
- app.use('*', (c, next)=>{
245
+ app.use('*', this.wrapMiddlewareWithFilters((c, next)=>{
194
246
  const path = c.req.path;
195
247
  const method = c.req.method;
196
248
  if (!matchRoute(path, method) || matchExclude(path, method)) return next();
@@ -202,7 +254,7 @@ export class RouteManager {
202
254
  return instance.use(c, ()=>runChain(index + 1)).then(()=>{});
203
255
  };
204
256
  return runChain(0);
205
- });
257
+ }));
206
258
  }
207
259
  // First pass: register all custom routes (must come before CRUD /:id routes).
208
260
  for (const { controller, metadata, routes } of this.controllers){
@@ -215,11 +267,11 @@ export class RouteManager {
215
267
  const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
216
268
  for (const fullPath of versionedPaths){
217
269
  for (const middlewareItem of middlewareItems){
218
- app.use(fullPath, (c, next)=>{
270
+ app.use(fullPath, this.wrapMiddlewareWithFilters((c, next)=>{
219
271
  const requestContainer = this.getRequestContainer(c);
220
272
  const resolved = instantiate(middlewareItem, requestContainer);
221
273
  return resolved.use(c, next);
222
- });
274
+ }));
223
275
  }
224
276
  this.registerRoute(app, route.method, fullPath, handler);
225
277
  }
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter,
9
9
  export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin, } from './container/index';
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
- 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';
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, createLazyParamDecorator, applyDecorators, } from './http/index';
13
13
  export { REQUEST_CONTEXT } from './http/request-context';
14
14
  export type { RequestContext } from './http/request-context';
15
15
  export { Logger, LogLevel } from './services/index';
@@ -41,7 +41,7 @@ export type { Constructor, MiddlewareType, GuardType, PipeType, InterceptorType,
41
41
  export { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe, ParseArrayPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe, } from './pipeline/index';
42
42
  export type { ParseUUIDPipeOptions, ParseArrayPipeOptions } from './pipeline/index';
43
43
  export { HttpException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, GoneException, PayloadTooLargeException, UnsupportedMediaTypeException, UnprocessableEntityException, TooManyRequestsException, InternalServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, GatewayTimeoutException, } from './errors/index';
44
- export type { OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, OnApplicationShutdown, BeforeApplicationShutdown, } from './lifecycle/index';
44
+ export type { OnModuleInit, OnApplicationBootstrap, OnFirstRequest, OnModuleDestroy, OnApplicationShutdown, BeforeApplicationShutdown, } from './lifecycle/index';
45
45
  export { MetadataRegistry } from './registry/metadata.registry';
46
46
  export { createZodDto, ValidationPipe } from './validation/index';
47
47
  export type { CreateZodDtoOptions } from './validation/index';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
11
11
  // Constants
12
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
13
13
  // HTTP Decorators
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";
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, createLazyParamDecorator, applyDecorators } from "./http/index.js";
15
15
  // Request-scoped context primitive
16
16
  export { REQUEST_CONTEXT } from "./http/request-context.js";
17
17
  // Services
@@ -4,6 +4,25 @@ export interface OnModuleInit {
4
4
  export interface OnApplicationBootstrap {
5
5
  onApplicationBootstrap(): void | Promise<void>;
6
6
  }
7
+ /**
8
+ * Fires exactly once, on the first incoming HTTP request, AFTER user-supplied
9
+ * middleware (so binding initializers like `@velajs/cloudflare`'s env capture
10
+ * run first) and BEFORE route handlers. The hook bridges module-load and
11
+ * request-time semantics — use it for state that depends on values only
12
+ * available at request time (Cloudflare bindings, Deno Deploy env, etc.).
13
+ *
14
+ * Concurrency: vela memoizes the call. Concurrent first requests all await
15
+ * the same promise; the hook runs exactly once.
16
+ *
17
+ * Non-HTTP consumers (CLI, tests) can trigger it manually via
18
+ * `app.callOnFirstRequest()`.
19
+ *
20
+ * Edge-safe: the hook itself is just `() => void | Promise<void>`. The
21
+ * runtime contract is async/Promise — no Node-only APIs are involved.
22
+ */
23
+ export interface OnFirstRequest {
24
+ onFirstRequest(): void | Promise<void>;
25
+ }
7
26
  export interface OnModuleDestroy {
8
27
  onModuleDestroy(): void | Promise<void>;
9
28
  }
@@ -15,6 +34,7 @@ export interface BeforeApplicationShutdown {
15
34
  }
16
35
  export declare function hasOnModuleInit(instance: unknown): instance is OnModuleInit;
17
36
  export declare function hasOnApplicationBootstrap(instance: unknown): instance is OnApplicationBootstrap;
37
+ export declare function hasOnFirstRequest(instance: unknown): instance is OnFirstRequest;
18
38
  export declare function hasOnModuleDestroy(instance: unknown): instance is OnModuleDestroy;
19
39
  export declare function hasBeforeApplicationShutdown(instance: unknown): instance is BeforeApplicationShutdown;
20
40
  export declare function hasOnApplicationShutdown(instance: unknown): instance is OnApplicationShutdown;
@@ -4,6 +4,9 @@ export function hasOnModuleInit(instance) {
4
4
  export function hasOnApplicationBootstrap(instance) {
5
5
  return instance !== null && typeof instance === 'object' && typeof instance.onApplicationBootstrap === 'function';
6
6
  }
7
+ export function hasOnFirstRequest(instance) {
8
+ return instance !== null && typeof instance === 'object' && typeof instance.onFirstRequest === 'function';
9
+ }
7
10
  export function hasOnModuleDestroy(instance) {
8
11
  return instance !== null && typeof instance === 'object' && typeof instance.onModuleDestroy === 'function';
9
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.5.1",
3
+ "version": "1.8.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",