@velajs/vela 1.9.0 → 1.12.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 (161) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +18 -0
  4. package/dist/application.js +45 -0
  5. package/dist/cache/cache.module.d.ts +5 -7
  6. package/dist/cache/cache.module.js +44 -66
  7. package/dist/cache/cache.types.d.ts +26 -0
  8. package/dist/cache/cache.types.js +1 -1
  9. package/dist/cache/index.d.ts +2 -1
  10. package/dist/cache/index.js +1 -0
  11. package/dist/cache/tiered-cache.store.d.ts +19 -0
  12. package/dist/cache/tiered-cache.store.js +40 -0
  13. package/dist/config/config.module.d.ts +6 -5
  14. package/dist/config/config.module.js +46 -49
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.js +223 -18
  17. package/dist/container/disposable.d.ts +5 -0
  18. package/dist/container/disposable.js +33 -0
  19. package/dist/container/types.d.ts +8 -0
  20. package/dist/cors/cors.module.d.ts +5 -5
  21. package/dist/cors/cors.module.js +26 -25
  22. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  23. package/dist/discovery/discoverable.decorator.js +42 -0
  24. package/dist/discovery/discovery.service.d.ts +94 -0
  25. package/dist/discovery/discovery.service.js +166 -0
  26. package/dist/discovery/index.d.ts +2 -0
  27. package/dist/discovery/index.js +2 -0
  28. package/dist/entrypoint/entrypoint.registry.d.ts +40 -0
  29. package/dist/entrypoint/entrypoint.registry.js +106 -0
  30. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  31. package/dist/entrypoint/entrypoint.types.js +3 -0
  32. package/dist/entrypoint/execution-context.d.ts +15 -0
  33. package/dist/entrypoint/execution-context.js +21 -0
  34. package/dist/entrypoint/execution-scope.d.ts +21 -0
  35. package/dist/entrypoint/execution-scope.js +28 -0
  36. package/dist/entrypoint/index.d.ts +4 -0
  37. package/dist/entrypoint/index.js +4 -0
  38. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  39. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  40. package/dist/factory/adapter.d.ts +36 -0
  41. package/dist/factory/adapter.js +18 -0
  42. package/dist/factory/bootstrap.js +20 -2
  43. package/dist/factory.d.ts +11 -1
  44. package/dist/factory.js +23 -2
  45. package/dist/fetch/fetch.module.d.ts +5 -6
  46. package/dist/fetch/fetch.module.js +9 -42
  47. package/dist/http/ambient.d.ts +17 -0
  48. package/dist/http/ambient.js +44 -0
  49. package/dist/http/execution-context.js +4 -1
  50. package/dist/http/handler-executor.js +22 -19
  51. package/dist/http/index.d.ts +1 -0
  52. package/dist/http/index.js +1 -0
  53. package/dist/http/route-contributor.d.ts +68 -0
  54. package/dist/http/route-contributor.js +22 -0
  55. package/dist/http/route.manager.d.ts +19 -0
  56. package/dist/http/route.manager.js +112 -19
  57. package/dist/i18n/deep-merge.d.ts +2 -0
  58. package/dist/i18n/deep-merge.js +15 -0
  59. package/dist/i18n/i18n.middleware.d.ts +15 -0
  60. package/dist/i18n/i18n.middleware.js +94 -0
  61. package/dist/i18n/i18n.module.d.ts +13 -0
  62. package/dist/i18n/i18n.module.js +64 -0
  63. package/dist/i18n/i18n.options.d.ts +30 -0
  64. package/dist/i18n/i18n.options.js +16 -0
  65. package/dist/i18n/i18n.service.d.ts +20 -0
  66. package/dist/i18n/i18n.service.js +49 -0
  67. package/dist/i18n/i18n.tokens.d.ts +6 -0
  68. package/dist/i18n/i18n.tokens.js +3 -0
  69. package/dist/i18n/i18n.types.d.ts +6 -0
  70. package/dist/i18n/i18n.types.js +1 -0
  71. package/dist/i18n/index.d.ts +10 -0
  72. package/dist/i18n/index.js +8 -0
  73. package/dist/i18n/message-loader.service.d.ts +21 -0
  74. package/dist/i18n/message-loader.service.js +90 -0
  75. package/dist/i18n/message-registry.d.ts +16 -0
  76. package/dist/i18n/message-registry.js +49 -0
  77. package/dist/index.d.ts +17 -5
  78. package/dist/index.js +14 -3
  79. package/dist/internal.d.ts +0 -2
  80. package/dist/internal.js +0 -7
  81. package/dist/module/configurable-module.builder.d.ts +56 -0
  82. package/dist/module/configurable-module.builder.js +84 -0
  83. package/dist/module/configurable-module.types.d.ts +93 -0
  84. package/dist/module/configurable-module.types.js +5 -0
  85. package/dist/module/define-module.d.ts +105 -0
  86. package/dist/module/define-module.js +227 -0
  87. package/dist/module/index.d.ts +4 -0
  88. package/dist/module/index.js +3 -0
  89. package/dist/module/lazy-provider.d.ts +65 -0
  90. package/dist/module/lazy-provider.js +111 -0
  91. package/dist/openapi/document.js +16 -15
  92. package/dist/pipeline/component.manager.d.ts +15 -9
  93. package/dist/pipeline/component.manager.js +32 -46
  94. package/dist/pipeline/index.d.ts +2 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  97. package/dist/pipeline/pipeline-runner.js +40 -0
  98. package/dist/pipeline/types.d.ts +21 -4
  99. package/dist/registry/metadata.registry.d.ts +24 -18
  100. package/dist/registry/metadata.registry.js +137 -49
  101. package/dist/schedule/schedule.module.d.ts +5 -0
  102. package/dist/schedule/schedule.module.js +18 -8
  103. package/dist/schedule/schedule.registry.d.ts +3 -3
  104. package/dist/schedule/schedule.registry.js +23 -44
  105. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  106. package/dist/schedule-node/schedule-node.module.js +20 -10
  107. package/dist/seeder/index.d.ts +5 -0
  108. package/dist/seeder/index.js +4 -0
  109. package/dist/seeder/seeder.decorator.d.ts +7 -0
  110. package/dist/seeder/seeder.decorator.js +15 -0
  111. package/dist/seeder/seeder.module.d.ts +16 -0
  112. package/dist/seeder/seeder.module.js +41 -0
  113. package/dist/seeder/seeder.registry.d.ts +23 -0
  114. package/dist/seeder/seeder.registry.js +83 -0
  115. package/dist/seeder/seeder.tokens.d.ts +2 -0
  116. package/dist/seeder/seeder.tokens.js +1 -0
  117. package/dist/seeder/seeder.types.d.ts +20 -0
  118. package/dist/seeder/seeder.types.js +1 -0
  119. package/dist/storage/index.d.ts +4 -0
  120. package/dist/storage/index.js +2 -0
  121. package/dist/storage/path-template.d.ts +9 -0
  122. package/dist/storage/path-template.js +20 -0
  123. package/dist/storage/signed-url.d.ts +12 -0
  124. package/dist/storage/signed-url.js +55 -0
  125. package/dist/storage/storage.types.d.ts +41 -0
  126. package/dist/storage/storage.types.js +5 -0
  127. package/dist/throttler/throttler.module.d.ts +5 -8
  128. package/dist/throttler/throttler.module.js +27 -55
  129. package/dist/websocket/index.d.ts +12 -0
  130. package/dist/websocket/index.js +16 -0
  131. package/dist/websocket/websocket.decorators.d.ts +33 -0
  132. package/dist/websocket/websocket.decorators.js +58 -0
  133. package/dist/websocket/websocket.module.d.ts +35 -0
  134. package/dist/websocket/websocket.module.js +63 -0
  135. package/dist/websocket/websocket.tokens.d.ts +23 -0
  136. package/dist/websocket/websocket.tokens.js +21 -0
  137. package/dist/websocket/websocket.types.d.ts +86 -0
  138. package/dist/websocket/websocket.types.js +4 -0
  139. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  140. package/dist/websocket/ws-argument-resolver.js +40 -0
  141. package/dist/websocket/ws-dispatcher.d.ts +45 -0
  142. package/dist/websocket/ws-dispatcher.js +259 -0
  143. package/dist/websocket/ws-exception.d.ts +16 -0
  144. package/dist/websocket/ws-exception.js +32 -0
  145. package/dist/websocket/ws-execution-context.d.ts +9 -0
  146. package/dist/websocket/ws-execution-context.js +24 -0
  147. package/dist/websocket/ws-server.d.ts +31 -0
  148. package/dist/websocket/ws-server.js +74 -0
  149. package/dist/websocket/ws-sync.d.ts +47 -0
  150. package/dist/websocket/ws-sync.js +74 -0
  151. package/dist/websocket-node/index.d.ts +4 -0
  152. package/dist/websocket-node/index.js +5 -0
  153. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  154. package/dist/websocket-node/node-ws-client.js +49 -0
  155. package/dist/websocket-node/redis-sync.d.ts +33 -0
  156. package/dist/websocket-node/redis-sync.js +56 -0
  157. package/dist/websocket-node/register-gateways.d.ts +20 -0
  158. package/dist/websocket-node/register-gateways.js +61 -0
  159. package/package.json +31 -1
  160. package/dist/http/crud-bridge.d.ts +0 -90
  161. package/dist/http/crud-bridge.js +0 -25
package/CHANGELOG.md CHANGED
@@ -1,5 +1,127 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0 (2026-07-04)
4
+
5
+ The module-model release: one blessed authoring path plus public kernel
6
+ extension points, so feature modules (websocket, storage, queue, …) are built
7
+ entirely on the public API. See `MODULE_AUTHORING.md` for the author contract.
8
+ Contains deliberate breaking changes (no deprecation shims); coordinated
9
+ releases of `@velajs/{storage,better-auth,testing,crud,cloudflare}` accompany
10
+ this version.
11
+
12
+ ### Added
13
+
14
+ - **`defineModule`** — the single module-authoring engine: generates `forRoot`
15
+ AND `forRootAsync` (typed `inject` inference), derives deterministic
16
+ `stableHash` keys (`key(options)` override + explicit `key` passthrough),
17
+ and accepts contributions (providers/controllers/imports/exports) **as
18
+ functions of the options** plus a standardized `global:` component slot.
19
+ `ConfigurableModuleBuilder` is now a thin adapter over it (unchanged API).
20
+ - **Authoring primitives**: `lazyProvider` (memoized deferred thunk — replaces
21
+ the hand-rolled `(...deps)=>()=>fn(...deps)` closures), `provideGlobal`
22
+ (the one `APP_*` wiring idiom), `sideEffectModule` (first-class
23
+ contribution-only modules; supported form of the i18n empty-marker trick),
24
+ `moduleToken`, `moduleKey`.
25
+ - **`DiscoveryService` + `createDiscoverableDecorator`** — public
26
+ decorator-driven discovery (`providersWithMeta`, `methodsWithMeta`,
27
+ `getProviders`) backed by a reverse metadata index inside
28
+ `MetadataRegistry`; honors container diagnostics in one place;
29
+ request-scoped providers are surfaced but not materialized (opt in with
30
+ `includeRequestScoped`). The event-emitter, schedule, and websocket
31
+ bootstrap scans now all run through it.
32
+ - **Open entrypoint registry** — `registerEntrypointKind({ kind, metaKey,
33
+ level })`, the `ContributesEntrypoints` interface, and per-application
34
+ `app.entrypoints` (`ofKind`/`kinds`/`all`), built at the end of
35
+ `callOnApplicationBootstrap()` so slim bootstrap paths (Cloudflare Durable
36
+ Objects) get it too. Transports query entrypoints instead of module
37
+ internals; a new kind (queue, cron, CLI) needs **zero core changes**.
38
+ - **`RouteContributor`** — public metadata-claimed route generation
39
+ (`registerRouteContributor`), consulted after explicit routes and during
40
+ OpenAPI generation with verb-level merge. Replaces the internal CrudBridge.
41
+ - **`RuntimeAdapter`** — `VelaFactory.create(module, { adapters: [...] })`
42
+ with `requestMiddleware` (prepended to the global chain), `onBootstrap`
43
+ (after lifecycle + entrypoints, before routes) and `onRoutesBuilt` hooks.
44
+ - **`PipelineRunner`** — the shared guard → pipe → interceptor execution core
45
+ used by HTTP and WebSocket dispatch (and any custom dispatcher);
46
+ configurable args/guards order, transport-specific guard-rejection error.
47
+ - **`Container.replaceProvider(provider, { buckets })`** — supported
48
+ force-replace across module buckets (what test harnesses need).
49
+ - **`buildEntrypointExecutionContext(kind, class, handler, payload)`** — the
50
+ entrypoint sibling of the HTTP/WS execution contexts, so guards/
51
+ interceptors/filters written against `getClass()`/`getHandler()`/`getType()`
52
+ run unchanged around queue batches, scheduled ticks, and custom kinds
53
+ (`EntrypointExecutionContext.getPayload()`). `@velajs/cloudflare` dispatches
54
+ queue/scheduled handlers through `PipelineRunner` with consumer-scoped
55
+ components (HTTP-global components deliberately do not apply; unclaimed
56
+ errors rethrow to preserve platform retry semantics).
57
+ - **`runInEntrypointScope(container, fn)`** — the non-HTTP dispatch scope
58
+ primitive: runs one unit of work (queue batch, scheduled tick, RPC call) in
59
+ a fresh request-scoped child with LIFO disposal — the per-request-child
60
+ equivalent for entrypoint dispatchers. `@velajs/cloudflare`'s queue and
61
+ scheduled handlers run on it (request-scoped consumer deps rebuild per
62
+ batch/tick instead of capturing boot instances).
63
+
64
+ ### Changed
65
+
66
+ - **Factory dependency visibility**: `useFactory`/`forRootAsync` `inject`
67
+ deps now resolve from the declaring module's scope FIRST (imports and
68
+ exports are honored), with the legacy no-requester lookup kept as fallback.
69
+ - **All in-core configurable modules are on the one engine**: `CorsModule` and
70
+ `SeederModule` rebuilt on `defineModule` (Cors gains `forRootAsync`; both
71
+ keep their token/key identities), `ScheduleModule`/`ScheduleNodeModule`
72
+ normalized to zero-config `@Module` bags with parity `forRoot()` sugar.
73
+ Builder-based modules (Config/Cache/I18n/Throttler/Http) already run on it
74
+ through the `ConfigurableModuleBuilder` adapter.
75
+ - **Unhandled handler errors are logged**: an error no exception filter claims
76
+ still maps to the generic 500 response, but the cause now lands in the logs
77
+ (`console.error`, gated on container diagnostics ≠ `silent`) — closing the
78
+ silent-500 gap.
79
+ - **`WebSocketModule`** rebuilt on `defineModule`: registry → driver → server
80
+ construction moved into chained provider factories (single shared registry
81
+ preserved; everything materializes at bootstrap), deterministic key
82
+ `ws#<sync-kind>` — **two identical `forRoot()` calls now dedup**
83
+ (HMR-idempotent; pass explicit `key` for exotic multi-instance),
84
+ `forRootAsync` available. `WsDispatcher` contributes `'websocket'`
85
+ entrypoints; `registerWebSocketGateways` consumes
86
+ `app.entrypoints.ofKind('websocket')`.
87
+
88
+ ### Breaking
89
+
90
+ - **`WS_MODULE_OPTIONS`** is now a typed `InjectionToken` (was the raw string
91
+ `'vela:ws-module-options'`).
92
+ - **`ComponentManager`** is stateless: `init()` and the process-global
93
+ container are gone; `resolve*` methods require an explicit container;
94
+ `getComponents` replaced by `getScopedComponents` (controller + handler
95
+ only — app-wide components have one source: `RouteManager`).
96
+ `registerGlobal`/`MetadataRegistry.getGlobal` (a dead tier with no readers
97
+ on the request path) are removed; `MetadataRegistry.clear()` is now a
98
+ no-op.
99
+ - **CrudBridge removed** (`registerCrudBridge`/`getCrudBridge` and the
100
+ `/internal` exports): use `registerRouteContributor`. `@velajs/crud`
101
+ migrates in its coordinated release.
102
+ - `forRootAsync` structural fields (non-async keys passed alongside
103
+ `useFactory`) now merge under the resolved options.
104
+
105
+ ## 1.10.0 (2026-07-01)
106
+
107
+ ### Added
108
+
109
+ - **WebSocket support** (`@velajs/vela/websocket` + `@velajs/vela/websocket-node`): `@WebSocketGateway`, `@SubscribeMessage`, gateways run through the same global guard/pipe/interceptor/filter tiers as HTTP (`RouteManager.getGlobalComponents()`).
110
+
111
+ - **`ConfigurableModuleBuilder`** (NestJS-parity) + the lower-level `defineConfigurableModule` engine. Generates `forRoot`/`forRootAsync` (with `key`, `global`, `useClass`/`useExisting` async options, and the options token) from a tiny spec, so a new module is just its tokens + options type + service + a `@Module({...})` bag — while keeping vela's module encapsulation and multi-instance `key` dedup. `CacheModule`, `ConfigModule`, `ThrottlerModule`, and `HttpModule` are migrated onto it; `@velajs/cloudflare`'s binding modules reuse the engine.
112
+ - **Opt-in ambient request access**: `getCurrentContainer()` / `getCurrentRequestContext()` + `enableAmbientContainer()`, wired via `VelaFactory.create(module, { ambientContainer: true })`. Backed by Hono's `hono/context-storage` (no `node:async_hooks` import in core); OFF by default, explicit child-container path unchanged. On Cloudflare Workers it requires the `nodejs_als` (or `nodejs_compat`) flag — validated on real workerd.
113
+ - **Container + application disposal**: `Container.dispose()` (LIFO; `Symbol.asyncDispose`/`Symbol.dispose`/`.dispose()`, idempotent), `VelaApplication.dispose()` and `Symbol.asyncDispose` (enables `await using`). The root disposes shared singletons; the per-request child container is disposed automatically at the end of each HTTP request — streaming-safe (deferred until the response body drains) and zero-overhead when a request has no request-scoped disposables.
114
+ - **Async cache stores (additive, non-breaking).** New `AsyncCacheStore` interface + `TieredCacheStore` (read-through + backfill + write-through over N sync/async tiers) in core, and `KVCacheStore` in `@velajs/cloudflare`, for a memory→KV cache. The existing synchronous `CacheStore`/`CacheService`/`CacheInterceptor` are **unchanged**; `CacheModuleOptions` gains an optional sync `store`. Use the async stores programmatically (inject under your own token).
115
+ - **i18n** at the `@velajs/vela/i18n` subpath (keeps core lean; `intl-messageformat` is an optional peer dep): `I18nModule.forRoot()` + `registerMessages()` (deep-merged, HMR-safe globalThis registry), ICU formatting, header/query/cookie locale detection. `I18nService` is request-scoped and reads the per-request locale; injecting it into a controller is safe thanks to request-scope bubbling (no `ambientContainer` flag needed).
116
+ - **Storage abstractions** at the `@velajs/vela/storage` subpath (dep-free, Web Crypto only): the `StorageDriver` contract, `expandPathTemplate`/`joinStoragePath`, and HMAC `signUrl`/`verifySignedUrl`. `@velajs/cloudflare` builds on them with a multi-disk `StorageModule` over R2 (`StorageService.put/get/delete/exists/url`, per-disk roots with `{date}`/`{year}`/… templates, bucket-by-name via `EnvService`) plus a signature-gated `StorageController` presign-proxy (R2 has no native presign).
117
+ - **Seeders** at the `@velajs/vela/seeder` subpath: `@Seeder({ order })`, `SeederModule` (`forRoot({ seeders })`), and `SeederRegistry`/`runSeeders(app)` — decorator discovery at bootstrap (mirrors `ScheduleRegistry`), each seeder run in a request-scoped child. Paired with the **new `@velajs/cli` package** (clipanion) providing `vela db seed` driven by a `vela.config.{js,mjs,ts}` app factory (Node-side; kept out of the Worker bundle).
118
+
119
+ ### Changed
120
+
121
+ - **Request-scope bubbling.** A provider (including controllers, which are singletons) that transitively depends on a request-scoped provider is now automatically treated as request-scoped — rebuilt per request instead of capturing the first request's instance. Matches NestJS; computed once at bootstrap (`Container.computeEffectiveScopes`), governs caching + eager instantiation. Fixes the captive-dependency hazard for request-scoped services injected into controllers.
122
+ - **HMR-safe `MetadataRegistry`**: all backing state is now anchored on `globalThis` via `Symbol.for('vela:registry:v1')`, so a Vite dev re-eval reuses the state classes were decorated against (no split-brain: lost routes / spurious "not @Injectable" warnings / duplicated globals). No API change.
123
+ - **`CacheModule`** now accepts an optional custom sync `store` in its options.
124
+
3
125
  ## 1.8.1 (2026-05-14)
4
126
 
5
127
  ### Revert
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ fetch(request: Request): Promise<Response>;
3
+ };
4
+ export default _default;
5
+ declare global {
6
+ interface ExportedHandler<Env = unknown> {
7
+ fetch?(request: Request, env: Env, ctx: unknown): Response | Promise<Response>;
8
+ }
9
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Hono } from 'hono';
2
2
  import type { Container } from './container/container';
3
3
  import type { Token } from './container/types';
4
+ import { EntrypointRegistry } from './entrypoint/entrypoint.registry';
4
5
  import type { RouteManager } from './http/route.manager';
5
6
  import type { MountOpenApiOptions } from './openapi/types';
6
7
  import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
@@ -9,6 +10,8 @@ export declare class VelaApplication {
9
10
  private readonly routeManager;
10
11
  private instances;
11
12
  private honoApp;
13
+ private entrypointRegistry;
14
+ private disposed;
12
15
  constructor(container: Container, routeManager: RouteManager);
13
16
  /** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */
14
17
  initRoutes(): Promise<void>;
@@ -43,5 +46,20 @@ export declare class VelaApplication {
43
46
  mountOpenApi(options: MountOpenApiOptions): this;
44
47
  callOnModuleInit(): Promise<void>;
45
48
  callOnApplicationBootstrap(): Promise<void>;
49
+ /**
50
+ * Every entrypoint contributed by the module graph, grouped by kind —
51
+ * what runtime adapters/transports query instead of re-scanning providers:
52
+ * `app.entrypoints.ofKind('websocket')`.
53
+ */
54
+ get entrypoints(): EntrypointRegistry;
46
55
  close(signal?: string): Promise<void>;
56
+ /**
57
+ * Full teardown: run shutdown lifecycle hooks ({@link close}) then dispose
58
+ * container-held instances (LIFO) and clear cached singletons. Use for
59
+ * graceful shutdown and dev HMR so old and new DI graphs never coexist.
60
+ *
61
+ * On runtimes/TS supporting explicit resource management this is also exposed
62
+ * as `Symbol.asyncDispose`, enabling `await using app = await VelaFactory.create(...)`.
63
+ */
64
+ dispose(signal?: string): Promise<void>;
47
65
  }
@@ -1,3 +1,5 @@
1
+ import { DiscoveryService } from "./discovery/discovery.service.js";
2
+ import { EntrypointRegistry } from "./entrypoint/entrypoint.registry.js";
1
3
  import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
2
4
  import { renderScalarUi } from "./openapi/scalar-ui.js";
3
5
  import { renderSwaggerUi } from "./openapi/swagger-ui.js";
@@ -7,6 +9,8 @@ export class VelaApplication {
7
9
  routeManager;
8
10
  instances = [];
9
11
  honoApp = null;
12
+ entrypointRegistry = null;
13
+ disposed = false;
10
14
  constructor(container, routeManager){
11
15
  this.container = container;
12
16
  this.routeManager = routeManager;
@@ -136,6 +140,23 @@ export class VelaApplication {
136
140
  await instance.onApplicationBootstrap();
137
141
  }
138
142
  }
143
+ // Build the per-app entrypoint registry AFTER the hooks: dispatchers that
144
+ // implement ContributesEntrypoints (WsDispatcher) finish their own
145
+ // discovery inside onApplicationBootstrap. Built here — not in
146
+ // VelaFactory/initRoutes — so slim bootstrap paths that never build HTTP
147
+ // routes (the Cloudflare Durable Object) still get `app.entrypoints`.
148
+ const discovery = this.container.has(DiscoveryService) ? this.container.resolve(DiscoveryService) : new DiscoveryService(this.container);
149
+ this.entrypointRegistry = await EntrypointRegistry.build(discovery, this.instances);
150
+ }
151
+ /**
152
+ * Every entrypoint contributed by the module graph, grouped by kind —
153
+ * what runtime adapters/transports query instead of re-scanning providers:
154
+ * `app.entrypoints.ofKind('websocket')`.
155
+ */ get entrypoints() {
156
+ if (!this.entrypointRegistry) {
157
+ throw new Error('Entrypoints are not built yet — they are assembled at the end of ' + 'callOnApplicationBootstrap(). Finish bootstrapping before querying them.');
158
+ }
159
+ return this.entrypointRegistry;
139
160
  }
140
161
  async close(signal) {
141
162
  const reversed = [
@@ -157,4 +178,28 @@ export class VelaApplication {
157
178
  }
158
179
  }
159
180
  }
181
+ /**
182
+ * Full teardown: run shutdown lifecycle hooks ({@link close}) then dispose
183
+ * container-held instances (LIFO) and clear cached singletons. Use for
184
+ * graceful shutdown and dev HMR so old and new DI graphs never coexist.
185
+ *
186
+ * On runtimes/TS supporting explicit resource management this is also exposed
187
+ * as `Symbol.asyncDispose`, enabling `await using app = await VelaFactory.create(...)`.
188
+ */ async dispose(signal) {
189
+ // Idempotent: `await using` + an explicit dispose(), or repeated shutdown
190
+ // signals, must not re-run shutdown lifecycle hooks.
191
+ if (this.disposed) return;
192
+ this.disposed = true;
193
+ await this.close(signal);
194
+ await this.container.dispose();
195
+ }
196
+ }
197
+ // Attach the well-known async-dispose symbol at runtime (it is not in the
198
+ // ES2022 lib the project compiles against) so `await using` works where
199
+ // supported, without a type dependency on esnext.disposable.
200
+ const ASYNC_DISPOSE = Symbol.asyncDispose;
201
+ if (ASYNC_DISPOSE) {
202
+ VelaApplication.prototype[ASYNC_DISPOSE] = function() {
203
+ return this.dispose();
204
+ };
160
205
  }
@@ -1,9 +1,7 @@
1
- import type { AsyncModuleOptions, DynamicModule } from '../module/types';
2
1
  import type { CacheModuleOptions } from './cache.types';
3
- export declare class CacheModule {
4
- static forRoot(options?: CacheModuleOptions): DynamicModule;
5
- static forRootAsync(options: AsyncModuleOptions<CacheModuleOptions> & {
6
- isGlobal?: boolean;
7
- key?: string;
8
- }): DynamicModule;
2
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<CacheModuleOptions, "forRoot", "create", {
3
+ isGlobal: boolean;
4
+ }>;
5
+ export declare class CacheModule extends ConfigurableModuleClass {
9
6
  }
7
+ export {};
@@ -1,80 +1,58 @@
1
- import { stableHash } from "../module/stable-hash.js";
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { Module } from "../module/decorators.js";
8
+ import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
2
9
  import { APP_INTERCEPTOR } from "../pipeline/tokens.js";
3
10
  import { CacheInterceptor } from "./cache.interceptor.js";
4
11
  import { CacheService } from "./cache.service.js";
5
12
  import { MemoryCacheStore } from "./cache.store.js";
6
13
  import { CACHE_MANAGER, CACHE_MODULE_OPTIONS } from "./cache.tokens.js";
7
- export class CacheModule {
8
- static forRoot(options = {}) {
9
- const { ttl = 5, max = 100, isGlobal = false } = options;
10
- const store = new MemoryCacheStore(ttl, max);
11
- const providers = [
12
- {
13
- provide: CACHE_MANAGER,
14
- useValue: store
15
- },
14
+ // Reuse the public CACHE_MODULE_OPTIONS token so its identity (and the
15
+ // index.ts export) is unchanged. `isGlobal` here means "register the interceptor
16
+ // globally as APP_INTERCEPTOR" NOT `DynamicModule.global` so the extras
17
+ // transform is customized rather than using the default isGlobal→global.
18
+ const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder({
19
+ moduleName: 'Cache',
20
+ optionsInjectionToken: CACHE_MODULE_OPTIONS
21
+ }).setExtras({
22
+ isGlobal: false
23
+ }, (definition, { isGlobal })=>isGlobal ? {
24
+ ...definition,
25
+ providers: [
26
+ ...definition.providers ?? [],
16
27
  {
17
- provide: CACHE_MODULE_OPTIONS,
18
- useValue: options
19
- },
20
- CacheService,
21
- CacheInterceptor
22
- ];
23
- if (isGlobal) {
24
- providers.push({
25
28
  provide: APP_INTERCEPTOR,
26
29
  useExisting: CacheInterceptor
27
- });
28
- }
29
- return {
30
- module: CacheModule,
31
- key: stableHash(options),
32
- providers,
33
- exports: [
34
- CACHE_MANAGER,
35
- CACHE_MODULE_OPTIONS,
36
- CacheService,
37
- CacheInterceptor
38
- ]
39
- };
40
- }
41
- static forRootAsync(options) {
42
- const providers = [
43
- {
44
- provide: CACHE_MODULE_OPTIONS,
45
- useFactory: options.useFactory,
46
- inject: options.inject ?? []
47
- },
30
+ }
31
+ ]
32
+ } : definition).build();
33
+ export class CacheModule extends ConfigurableModuleClass {
34
+ }
35
+ CacheModule = _ts_decorate([
36
+ Module({
37
+ providers: [
38
+ CacheService,
39
+ CacheInterceptor,
40
+ // One options-injecting provider serves BOTH forRoot and forRootAsync.
41
+ // A custom `store` (e.g. TieredCacheStore / KVCacheStore) overrides the
42
+ // default in-memory store.
48
43
  {
49
44
  provide: CACHE_MANAGER,
50
- useFactory: (opts)=>new MemoryCacheStore(opts.ttl ?? 5, opts.max ?? 100),
45
+ useFactory: (options)=>options.store ?? new MemoryCacheStore(options.ttl ?? 5, options.max ?? 100),
51
46
  inject: [
52
- CACHE_MODULE_OPTIONS
47
+ MODULE_OPTIONS_TOKEN
53
48
  ]
54
- },
49
+ }
50
+ ],
51
+ exports: [
52
+ CACHE_MANAGER,
53
+ CACHE_MODULE_OPTIONS,
55
54
  CacheService,
56
55
  CacheInterceptor
57
- ];
58
- if (options.isGlobal) {
59
- providers.push({
60
- provide: APP_INTERCEPTOR,
61
- useExisting: CacheInterceptor
62
- });
63
- }
64
- return {
65
- module: CacheModule,
66
- key: options.key ?? stableHash({
67
- inject: options.inject,
68
- useFactory: options.useFactory
69
- }),
70
- imports: options.imports ?? [],
71
- providers,
72
- exports: [
73
- CACHE_MANAGER,
74
- CACHE_MODULE_OPTIONS,
75
- CacheService,
76
- CacheInterceptor
77
- ]
78
- };
79
- }
80
- }
56
+ ]
57
+ })
58
+ ], CacheModule);
@@ -1,14 +1,40 @@
1
+ /** A value that may be returned synchronously or as a promise. */
2
+ export type Awaitable<T> = T | Promise<T>;
1
3
  export interface CacheModuleOptions {
2
4
  ttl?: number;
3
5
  max?: number;
4
6
  isGlobal?: boolean;
7
+ /**
8
+ * Custom SYNC backing store (replaces the default in-memory store). For async
9
+ * (KV/tiered) caching use {@link AsyncCacheStore} + `TieredCacheStore`
10
+ * programmatically — the interceptor/CacheService path is synchronous.
11
+ */
12
+ store?: CacheStore;
5
13
  }
14
+ /**
15
+ * Synchronous cache backing store. This is the original, stable contract used by
16
+ * `CacheService` and `CacheInterceptor` — unchanged.
17
+ */
6
18
  export interface CacheStore {
7
19
  get<T = unknown>(key: string): T | undefined;
8
20
  set<T = unknown>(key: string, value: T, ttl?: number): void;
9
21
  del(key: string): void;
10
22
  clear(): void;
11
23
  }
24
+ /**
25
+ * Asynchronous cache backing store (additive). Implemented by remote/tiered
26
+ * stores (`TieredCacheStore`, and `KVCacheStore` in `@velajs/cloudflare`) where
27
+ * reads/writes are inherently async. Use it programmatically — inject the store
28
+ * under your own token — rather than as the synchronous `CACHE_MANAGER`.
29
+ */
30
+ export interface AsyncCacheStore {
31
+ get<T = unknown>(key: string): Promise<T | undefined>;
32
+ set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
33
+ del(key: string): Promise<void>;
34
+ clear(): Promise<void>;
35
+ }
36
+ /** A store that may be sync or async (used to compose tiers). */
37
+ export type AnyCacheStore = CacheStore | AsyncCacheStore;
12
38
  export interface CacheEntry<T = unknown> {
13
39
  value: T;
14
40
  expiresAt: number;
@@ -1 +1 @@
1
- export { };
1
+ /** A value that may be returned synchronously or as a promise. */ export { };
@@ -2,6 +2,7 @@ export { CacheModule } from './cache.module';
2
2
  export { CacheService } from './cache.service';
3
3
  export { CacheInterceptor } from './cache.interceptor';
4
4
  export { MemoryCacheStore } from './cache.store';
5
+ export { TieredCacheStore } from './tiered-cache.store';
5
6
  export { CacheKey, CacheTTL } from './cache.decorators';
6
7
  export { CACHE_MANAGER, CACHE_MODULE_OPTIONS, CACHE_KEY_METADATA, CACHE_TTL_METADATA } from './cache.tokens';
7
- export type { CacheModuleOptions, CacheStore, CacheEntry } from './cache.types';
8
+ export type { Awaitable, CacheModuleOptions, CacheStore, AsyncCacheStore, AnyCacheStore, CacheEntry, } from './cache.types';
@@ -2,5 +2,6 @@ export { CacheModule } from "./cache.module.js";
2
2
  export { CacheService } from "./cache.service.js";
3
3
  export { CacheInterceptor } from "./cache.interceptor.js";
4
4
  export { MemoryCacheStore } from "./cache.store.js";
5
+ export { TieredCacheStore } from "./tiered-cache.store.js";
5
6
  export { CacheKey, CacheTTL } from "./cache.decorators.js";
6
7
  export { CACHE_MANAGER, CACHE_MODULE_OPTIONS, CACHE_KEY_METADATA, CACHE_TTL_METADATA } from "./cache.tokens.js";
@@ -0,0 +1,19 @@
1
+ import type { AnyCacheStore, AsyncCacheStore } from './cache.types';
2
+ /**
3
+ * Composes an ordered list of cache tiers (fastest first, e.g. in-memory L1 →
4
+ * KV L2). Tiers may be sync ({@link CacheStore}) or async ({@link AsyncCacheStore}) —
5
+ * results are awaited uniformly. Reads are read-through: the first tier with a
6
+ * hit wins, and nearer tiers that missed are backfilled. Writes/deletes/clears
7
+ * fan out to all tiers. Always async ({@link AsyncCacheStore}).
8
+ *
9
+ * Runtime-agnostic — the concrete tiers (memory here, KV in `@velajs/cloudflare`)
10
+ * live wherever fits.
11
+ */
12
+ export declare class TieredCacheStore implements AsyncCacheStore {
13
+ private readonly tiers;
14
+ constructor(tiers: AnyCacheStore[]);
15
+ get<T = unknown>(key: string): Promise<T | undefined>;
16
+ set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
17
+ del(key: string): Promise<void>;
18
+ clear(): Promise<void>;
19
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Composes an ordered list of cache tiers (fastest first, e.g. in-memory L1 →
3
+ * KV L2). Tiers may be sync ({@link CacheStore}) or async ({@link AsyncCacheStore}) —
4
+ * results are awaited uniformly. Reads are read-through: the first tier with a
5
+ * hit wins, and nearer tiers that missed are backfilled. Writes/deletes/clears
6
+ * fan out to all tiers. Always async ({@link AsyncCacheStore}).
7
+ *
8
+ * Runtime-agnostic — the concrete tiers (memory here, KV in `@velajs/cloudflare`)
9
+ * live wherever fits.
10
+ */ export class TieredCacheStore {
11
+ tiers;
12
+ constructor(tiers){
13
+ if (tiers.length === 0) {
14
+ throw new Error('TieredCacheStore requires at least one tier.');
15
+ }
16
+ this.tiers = tiers;
17
+ }
18
+ async get(key) {
19
+ for(let i = 0; i < this.tiers.length; i++){
20
+ const value = await this.tiers[i].get(key);
21
+ if (value !== undefined) {
22
+ // Backfill nearer (faster) tiers that missed.
23
+ for(let j = 0; j < i; j++){
24
+ await this.tiers[j].set(key, value);
25
+ }
26
+ return value;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+ async set(key, value, ttl) {
32
+ await Promise.all(this.tiers.map((tier)=>tier.set(key, value, ttl)));
33
+ }
34
+ async del(key) {
35
+ await Promise.all(this.tiers.map((tier)=>tier.del(key)));
36
+ }
37
+ async clear() {
38
+ await Promise.all(this.tiers.map((tier)=>tier.clear()));
39
+ }
40
+ }
@@ -1,11 +1,12 @@
1
- import type { AsyncModuleOptions, DynamicModule } from '../module/types';
1
+ import type { DynamicModule } from '../module/types';
2
2
  import type { ConfigModuleOptions } from './config.types';
3
- export declare class ConfigModule {
3
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<ConfigModuleOptions<Record<string, unknown>>, "forRoot", "create", {
4
+ isGlobal?: boolean;
5
+ }>;
6
+ export declare class ConfigModule extends ConfigurableModuleClass {
4
7
  static forRoot<T extends Record<string, unknown>>(options: ConfigModuleOptions<T> & {
5
- key?: string;
6
- }): DynamicModule;
7
- static forRootAsync<T extends Record<string, unknown>>(options: AsyncModuleOptions<ConfigModuleOptions<T>> & {
8
8
  isGlobal?: boolean;
9
9
  key?: string;
10
10
  }): DynamicModule;
11
11
  }
12
+ export {};
@@ -1,54 +1,51 @@
1
- import { stableHash } from "../module/stable-hash.js";
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { Module } from "../module/decorators.js";
8
+ import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
2
9
  import { ConfigService } from "./config.service.js";
3
10
  import { CONFIG_OPTIONS } from "./config.tokens.js";
4
- export class ConfigModule {
11
+ // MODULE_OPTIONS_TOKEN carries the RAW options; CONFIG_OPTIONS stays the
12
+ // distinct validated-record token, derived in @Module. The derived provider
13
+ // validates the forRootAsync path lazily (its factory is deferred); forRoot
14
+ // validates eagerly at call time (see the override below), matching the
15
+ // long-standing contract, and passes an already-validated config through.
16
+ const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder({
17
+ moduleName: 'Config'
18
+ }).build();
19
+ export class ConfigModule extends ConfigurableModuleClass {
20
+ // Preserve (a) per-call generic inference over the config record and (b) the
21
+ // eager-validation contract: forRoot validates at call time so bad config
22
+ // fails fast. `super.forRoot` runs the generated static with `this === ConfigModule`.
5
23
  static forRoot(options) {
6
- const config = options.validate ? options.validate(options.config) : options.config;
7
- return {
8
- module: ConfigModule,
9
- key: options.key ?? stableHash(options.config),
10
- providers: [
11
- {
12
- provide: CONFIG_OPTIONS,
13
- useValue: config
14
- },
15
- ConfigService
16
- ],
17
- exports: [
18
- ConfigService,
19
- CONFIG_OPTIONS
20
- ],
21
- ...options.isGlobal ? {
22
- global: true
23
- } : {}
24
- };
25
- }
26
- static forRootAsync(options) {
27
- return {
28
- module: ConfigModule,
29
- key: options.key ?? stableHash({
30
- inject: options.inject,
31
- useFactory: options.useFactory
32
- }),
33
- imports: options.imports ?? [],
34
- providers: [
35
- {
36
- provide: CONFIG_OPTIONS,
37
- useFactory: async (...args)=>{
38
- const opts = await options.useFactory(...args);
39
- return opts.validate ? opts.validate(opts.config) : opts.config;
40
- },
41
- inject: options.inject ?? []
42
- },
43
- ConfigService
44
- ],
45
- exports: [
46
- ConfigService,
47
- CONFIG_OPTIONS
48
- ],
49
- ...options.isGlobal ? {
50
- global: true
51
- } : {}
52
- };
24
+ const validated = options.validate ? options.validate(options.config) : options.config;
25
+ // Strip `validate` so the derived CONFIG_OPTIONS provider is a passthrough
26
+ // (no double validation) for the already-validated config.
27
+ const { validate: _validate, ...rest } = options;
28
+ return super.forRoot({
29
+ ...rest,
30
+ config: validated
31
+ });
53
32
  }
54
33
  }
34
+ ConfigModule = _ts_decorate([
35
+ Module({
36
+ providers: [
37
+ ConfigService,
38
+ {
39
+ provide: CONFIG_OPTIONS,
40
+ useFactory: (options)=>options.validate ? options.validate(options.config) : options.config,
41
+ inject: [
42
+ MODULE_OPTIONS_TOKEN
43
+ ]
44
+ }
45
+ ],
46
+ exports: [
47
+ ConfigService,
48
+ CONFIG_OPTIONS
49
+ ]
50
+ })
51
+ ], ConfigModule);