@velajs/vela 1.17.0 → 1.18.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.
@@ -65,7 +65,7 @@ export class ArgumentResolver {
65
65
  const metadata = {
66
66
  type: param.type,
67
67
  data: param.name,
68
- metatype: paramTypes?.[param.index]
68
+ metatype: param.metatype ?? paramTypes?.[param.index]
69
69
  };
70
70
  for (const pipe of pipes){
71
71
  value = await pipe.transform(value, metadata);
@@ -0,0 +1,16 @@
1
+ import type { Context } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ /**
4
+ * Returns the request-scoped child container for the current request.
5
+ *
6
+ * `@Inject(Container)` resolves the ROOT container (request children share the
7
+ * provider map and never rebind `Container`), so module authors who need
8
+ * request-scoped resolution from inside a handler, param-decorator factory, or
9
+ * scoped middleware must read the child seeded by RouteManager instead. The
10
+ * child is created before guards, pipes, params, and handlers run, so it is
11
+ * always present on the Vela request path.
12
+ *
13
+ * Throws outside a Vela-managed request rather than materializing an orphan
14
+ * child (mirrors the REQUEST_CONTEXT fail-fast).
15
+ */
16
+ export declare function getRequestContainer(c: Context): Container;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Returns the request-scoped child container for the current request.
3
+ *
4
+ * `@Inject(Container)` resolves the ROOT container (request children share the
5
+ * provider map and never rebind `Container`), so module authors who need
6
+ * request-scoped resolution from inside a handler, param-decorator factory, or
7
+ * scoped middleware must read the child seeded by RouteManager instead. The
8
+ * child is created before guards, pipes, params, and handlers run, so it is
9
+ * always present on the Vela request path.
10
+ *
11
+ * Throws outside a Vela-managed request rather than materializing an orphan
12
+ * child (mirrors the REQUEST_CONTEXT fail-fast).
13
+ */ export function getRequestContainer(c) {
14
+ const container = c.get('container');
15
+ if (!container) {
16
+ throw new Error('getRequestContainer(c) can only be called inside a Vela-managed request — ' + 'the request child container is seeded by RouteManager when the request enters the pipeline.');
17
+ }
18
+ return container;
19
+ }
@@ -405,7 +405,7 @@ export class RouteManager {
405
405
  // `await import(variable)` as a runtime import Cloudflare Workers cannot
406
406
  // resolve.
407
407
  const contributors = getRouteContributors();
408
- for (const { controller, metadata } of this.controllers){
408
+ for (const { controller, metadata, routes } of this.controllers){
409
409
  for (const contributor of contributors){
410
410
  const meta = getMetadata(contributor.claimsMetaKey, controller);
411
411
  if (meta === undefined) continue;
@@ -419,10 +419,11 @@ export class RouteManager {
419
419
  joinPaths
420
420
  });
421
421
  }
422
- // DX guard for the common mistake: @Crud() metadata present but the
423
- // contributor package never imported.
424
- if (getMetadata('vela:crud', controller) !== undefined && !contributors.some((c)=>c.claimsMetaKey === 'vela:crud')) {
425
- throw new Error("@Crud() requires '@velajs/crud'. Install it: pnpm add @velajs/crud");
422
+ // DX warning for stale setups: @Crud() metadata but nothing produced a
423
+ // route native @velajs/crud (>=1.18) stamps real routes at decoration
424
+ // time, and legacy bridges register a claiming contributor.
425
+ if (getMetadata('vela:crud', controller) !== undefined && routes.length === 0 && !contributors.some((c)=>c.claimsMetaKey === 'vela:crud')) {
426
+ console.warn(`[vela] ${controller.name} carries @Crud() metadata but produced no routes — ` + "install and import '@velajs/crud' (>=1.18), or remove the decorator.");
426
427
  }
427
428
  }
428
429
  return app;
@@ -23,6 +23,12 @@ export interface ParamMetadata {
23
23
  name?: string;
24
24
  pipes?: PipeType[];
25
25
  factory?: (data: unknown, ctx: import('hono').Context) => unknown;
26
+ /**
27
+ * Explicit param type for programmatic routes. Methods synthesized at
28
+ * runtime (e.g. `@Crud()` verb handlers) have no `design:paramtypes`, so
29
+ * ValidationPipe and the OpenAPI walk read this instead when present.
30
+ */
31
+ metatype?: unknown;
26
32
  }
27
33
  export interface ControllerRegistration {
28
34
  controller: Type;
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { signUrl, verifySignedUrl } from './crypto/signed-url';
16
16
  export type { SignedUrlOptions } from './crypto/signed-url';
17
17
  export { REQUEST_CONTEXT } from './http/request-context';
18
18
  export type { RequestContext } from './http/request-context';
19
+ export { getRequestContainer } from './http/request-container';
19
20
  export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './http/ambient';
20
21
  export { Logger, LogLevel } from './services/index';
21
22
  export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
package/dist/index.js CHANGED
@@ -16,6 +16,9 @@ export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All,
16
16
  export { signUrl, verifySignedUrl } from "./crypto/signed-url.js";
17
17
  // Request-scoped context primitive
18
18
  export { REQUEST_CONTEXT } from "./http/request-context.js";
19
+ // Explicit request-child container access (for programmatic-route authors,
20
+ // param-decorator factories, and scoped middleware)
21
+ export { getRequestContainer } from "./http/request-container.js";
19
22
  // Opt-in ambient container access (ALS via hono/context-storage)
20
23
  export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext } from "./http/ambient.js";
21
24
  // Services
@@ -1,7 +1,7 @@
1
1
  export { LiveModule } from './live.module';
2
2
  export { LiveResolver, LiveQuery, getLiveQueries } from './live.decorators';
3
3
  export { LiveEngine, LIVE_SUBS_DATA_KEY, readPersistedLiveSubscriptions } from './live.engine';
4
- export { LiveInvalidation, localLive, stampCommitHeaders } from './live.invalidation';
4
+ export { LiveInvalidation, localLive, perAppLiveDriver, stampCommitHeaders } from './live.invalidation';
5
5
  export { InMemoryCursorLog } from './live.cursor';
6
6
  export { encodeSubscriptionUpdate } from './live.delta';
7
7
  export { PresenceService, PresenceResolver, presenceTag, PRESENCE_ROSTER_QUERY } from './presence';
@@ -6,7 +6,7 @@
6
6
  export { LiveModule } from "./live.module.js";
7
7
  export { LiveResolver, LiveQuery, getLiveQueries } from "./live.decorators.js";
8
8
  export { LiveEngine, LIVE_SUBS_DATA_KEY, readPersistedLiveSubscriptions } from "./live.engine.js";
9
- export { LiveInvalidation, localLive, stampCommitHeaders } from "./live.invalidation.js";
9
+ export { LiveInvalidation, localLive, perAppLiveDriver, stampCommitHeaders } from "./live.invalidation.js";
10
10
  export { InMemoryCursorLog } from "./live.cursor.js";
11
11
  export { encodeSubscriptionUpdate } from "./live.delta.js";
12
12
  export { PresenceService, PresenceResolver, presenceTag, PRESENCE_ROSTER_QUERY } from "./presence.js";
@@ -1,6 +1,24 @@
1
1
  import type { CommitStamp, InvalidationCommand, LiveDriver } from './live.types';
2
2
  /** Single-scope default driver: deliver straight to this process's engine. */
3
3
  export declare function localLive(): LiveDriver;
4
+ /**
5
+ * Per-app wrapper around a (possibly SHARED) driver instance.
6
+ *
7
+ * A driver passed to `LiveModule.forRoot({ driver })` is a plain config object
8
+ * — and on Cloudflare the SAME app module (thus the same driver object)
9
+ * bootstraps in the Worker isolate AND inside each Durable Object, often
10
+ * within one isolate. Mutable per-app state must therefore never live on the
11
+ * driver itself: the DO flipping a shared `durableObjectLive()` into local
12
+ * mode would otherwise poison the Worker's engine, which then executes the
13
+ * DO's storage handle in the wrong request context ("Cannot perform I/O on
14
+ * behalf of a different request").
15
+ *
16
+ * The wrapper owns the per-app sink and the per-app local-mode flag; isolate-
17
+ * wide concerns (env capture) forward to the underlying driver. Platform init
18
+ * hooks (`_setLocalMode`, `_initializeEnv`) are exposed pass-through so
19
+ * platform packages can keep type-guarding on their presence.
20
+ */
21
+ export declare function perAppLiveDriver(underlying: LiveDriver): LiveDriver;
4
22
  /**
5
23
  * The injectable invalidation surface for custom (non-CRUD) write paths:
6
24
  *
@@ -12,6 +12,49 @@ import { getContext } from "hono/context-storage";
12
12
  }
13
13
  };
14
14
  }
15
+ /**
16
+ * Per-app wrapper around a (possibly SHARED) driver instance.
17
+ *
18
+ * A driver passed to `LiveModule.forRoot({ driver })` is a plain config object
19
+ * — and on Cloudflare the SAME app module (thus the same driver object)
20
+ * bootstraps in the Worker isolate AND inside each Durable Object, often
21
+ * within one isolate. Mutable per-app state must therefore never live on the
22
+ * driver itself: the DO flipping a shared `durableObjectLive()` into local
23
+ * mode would otherwise poison the Worker's engine, which then executes the
24
+ * DO's storage handle in the wrong request context ("Cannot perform I/O on
25
+ * behalf of a different request").
26
+ *
27
+ * The wrapper owns the per-app sink and the per-app local-mode flag; isolate-
28
+ * wide concerns (env capture) forward to the underlying driver. Platform init
29
+ * hooks (`_setLocalMode`, `_initializeEnv`) are exposed pass-through so
30
+ * platform packages can keep type-guarding on their presence.
31
+ */ export function perAppLiveDriver(underlying) {
32
+ let ownSink;
33
+ let localMode = false;
34
+ const cfHooks = underlying;
35
+ return {
36
+ get kind () {
37
+ return underlying.kind;
38
+ },
39
+ bind (sink) {
40
+ ownSink = sink;
41
+ underlying.bind(sink);
42
+ },
43
+ dispatch (cmd) {
44
+ if (localMode && ownSink) return ownSink.applyInvalidation(cmd);
45
+ return underlying.dispatch(cmd);
46
+ },
47
+ start: underlying.start ? ()=>underlying.start?.() : undefined,
48
+ stop: underlying.stop ? ()=>underlying.stop?.() : undefined,
49
+ // Platform hooks (Cloudflare): local mode is PER APP; env is per isolate.
50
+ _setLocalMode () {
51
+ localMode = true;
52
+ },
53
+ _initializeEnv (env) {
54
+ cfHooks._initializeEnv?.(env);
55
+ }
56
+ };
57
+ }
15
58
  /**
16
59
  * The injectable invalidation surface for custom (non-CRUD) write paths:
17
60
  *
@@ -1,7 +1,7 @@
1
1
  import { defineModule } from "../index.js";
2
2
  import { InMemoryCursorLog } from "./live.cursor.js";
3
3
  import { LiveEngine } from "./live.engine.js";
4
- import { LiveInvalidation, localLive } from "./live.invalidation.js";
4
+ import { LiveInvalidation, localLive, perAppLiveDriver } from "./live.invalidation.js";
5
5
  import { LIVE_CURSOR_LOG, LIVE_DRIVER, LIVE_MODULE_OPTIONS } from "./live.tokens.js";
6
6
  import { PresenceResolver, PresenceService } from "./presence.js";
7
7
  /**
@@ -39,7 +39,11 @@ import { PresenceResolver, PresenceService } from "./presence.js";
39
39
  },
40
40
  {
41
41
  provide: LIVE_DRIVER,
42
- useFactory: (o)=>o.driver ?? localLive(),
42
+ // Wrapped per app: user-supplied drivers are shared config objects
43
+ // (the same module bootstraps in the Worker AND in each Durable
44
+ // Object), so per-app sink/mode state lives in the wrapper — see
45
+ // perAppLiveDriver.
46
+ useFactory: (o)=>perAppLiveDriver(o.driver ?? localLive()),
43
47
  inject: [
44
48
  OPTIONS
45
49
  ]
@@ -39,7 +39,7 @@ function isDtoClass(value) {
39
39
  return typeof value === 'function' && value.schema != null;
40
40
  }
41
41
  function getParamSchema(param, paramtypes, registry) {
42
- const metatype = paramtypes?.[param.index];
42
+ const metatype = param.metatype ?? paramtypes?.[param.index];
43
43
  if (!metatype) return undefined;
44
44
  if (isDtoClass(metatype)) {
45
45
  return registry.ref(metatype);
@@ -51,7 +51,7 @@ function getParamSchema(param, paramtypes, registry) {
51
51
  return undefined;
52
52
  }
53
53
  function isParamOptional(param, paramtypes) {
54
- const metatype = paramtypes?.[param.index];
54
+ const metatype = param.metatype ?? paramtypes?.[param.index];
55
55
  if (metatype?.schema) {
56
56
  return isOptional(metatype.schema);
57
57
  }
@@ -30,6 +30,12 @@ export interface ParameterMetadata {
30
30
  name?: string;
31
31
  pipes?: PipeType[];
32
32
  factory?: (data: unknown, ctx: Context) => unknown;
33
+ /**
34
+ * Explicit param type for programmatic routes. Methods synthesized at
35
+ * runtime (e.g. `@Crud()` verb handlers) have no `design:paramtypes`, so
36
+ * ValidationPipe and the OpenAPI walk read this instead when present.
37
+ */
38
+ metatype?: unknown;
33
39
  }
34
40
  export interface HttpHandlerMeta {
35
41
  httpCode?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",