@velajs/vela 0.4.2 → 0.4.3

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.
@@ -1,3 +1,4 @@
1
+ import { Scope } from '../constants';
1
2
  import type { ProviderOptions, Token, Type } from './types';
2
3
  export declare class Container {
3
4
  private providers;
@@ -9,6 +10,7 @@ export declare class Container {
9
10
  private registerOptions;
10
11
  resolve<T>(token: Token<T>): T;
11
12
  has(token: Token): boolean;
13
+ getProviderScope(token: Token): Scope | undefined;
12
14
  getTokens(): Token[];
13
15
  /**
14
16
  * Create a child container for request-scoped resolution.
@@ -40,6 +40,8 @@ export class Container {
40
40
  } else if (options.useFactory) {
41
41
  registration.useFactory = options.useFactory;
42
42
  registration.inject = options.inject;
43
+ } else if (options.useClass) {
44
+ registration.useClass = options.useClass;
43
45
  } else if (options.useExisting) {
44
46
  registration.useExisting = options.useExisting;
45
47
  } else if (typeof token === 'function') {
@@ -68,6 +70,9 @@ export class Container {
68
70
  has(token) {
69
71
  return this.providers.has(token);
70
72
  }
73
+ getProviderScope(token) {
74
+ return this.providers.get(token)?.scope;
75
+ }
71
76
  getTokens() {
72
77
  return Array.from(this.providers.keys());
73
78
  }
@@ -22,6 +22,7 @@ export interface ProviderOptions<T = unknown> {
22
22
  scope?: Scope;
23
23
  useValue?: T;
24
24
  useFactory?: (...args: any[]) => T | Promise<T>;
25
+ useClass?: Type<T>;
25
26
  inject?: Token[];
26
27
  useExisting?: Token<T>;
27
28
  }
package/dist/factory.js CHANGED
@@ -15,21 +15,35 @@ export const VelaFactory = {
15
15
  ComponentManager.init(container);
16
16
  const loader = new ModuleLoader(container, routeManager);
17
17
  loader.load(rootModule);
18
- // Resolve APP_* tokens registered via module providers
19
- if (container.has(APP_GUARD)) {
20
- routeManager.useGlobalGuards(container.resolve(APP_GUARD));
18
+ const appGuards = loader.getAppProviderTokens(APP_GUARD);
19
+ if (appGuards.length > 0) {
20
+ routeManager.useGlobalGuardTokens(...appGuards);
21
+ } else if (container.has(APP_GUARD)) {
22
+ routeManager.useGlobalGuardTokens(APP_GUARD);
21
23
  }
22
- if (container.has(APP_PIPE)) {
23
- routeManager.useGlobalPipes(container.resolve(APP_PIPE));
24
+ const appPipes = loader.getAppProviderTokens(APP_PIPE);
25
+ if (appPipes.length > 0) {
26
+ routeManager.useGlobalPipeTokens(...appPipes);
27
+ } else if (container.has(APP_PIPE)) {
28
+ routeManager.useGlobalPipeTokens(APP_PIPE);
24
29
  }
25
- if (container.has(APP_INTERCEPTOR)) {
26
- routeManager.useGlobalInterceptors(container.resolve(APP_INTERCEPTOR));
30
+ const appInterceptors = loader.getAppProviderTokens(APP_INTERCEPTOR);
31
+ if (appInterceptors.length > 0) {
32
+ routeManager.useGlobalInterceptorTokens(...appInterceptors);
33
+ } else if (container.has(APP_INTERCEPTOR)) {
34
+ routeManager.useGlobalInterceptorTokens(APP_INTERCEPTOR);
27
35
  }
28
- if (container.has(APP_FILTER)) {
29
- routeManager.useGlobalFilters(container.resolve(APP_FILTER));
36
+ const appFilters = loader.getAppProviderTokens(APP_FILTER);
37
+ if (appFilters.length > 0) {
38
+ routeManager.useGlobalFilterTokens(...appFilters);
39
+ } else if (container.has(APP_FILTER)) {
40
+ routeManager.useGlobalFilterTokens(APP_FILTER);
30
41
  }
31
- if (container.has(APP_MIDDLEWARE)) {
32
- routeManager.useGlobalMiddleware(container.resolve(APP_MIDDLEWARE));
42
+ const appMiddleware = loader.getAppProviderTokens(APP_MIDDLEWARE);
43
+ if (appMiddleware.length > 0) {
44
+ routeManager.useGlobalMiddlewareTokens(...appMiddleware);
45
+ } else if (container.has(APP_MIDDLEWARE)) {
46
+ routeManager.useGlobalMiddlewareTokens(APP_MIDDLEWARE);
33
47
  }
34
48
  const app = new VelaApplication(container, routeManager);
35
49
  const instances = loader.resolveAllInstances();
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { Container } from '../container/container';
3
- import type { Type } from '../container/types';
3
+ import type { Token, Type } from '../container/types';
4
+ import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
4
5
  import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from '../registry/types';
5
6
  import type { ControllerRegistration } from './types';
6
7
  export declare class RouteManager {
@@ -15,11 +16,18 @@ export declare class RouteManager {
15
16
  constructor(container: Container);
16
17
  setGlobalPrefix(prefix: string): this;
17
18
  useGlobalMiddleware(...middleware: MiddlewareType[]): this;
19
+ useGlobalMiddlewareTokens(...middlewareTokens: Array<Token<NestMiddleware>>): this;
18
20
  useGlobalPipes(...pipes: PipeType[]): this;
21
+ useGlobalPipeTokens(...pipeTokens: Array<Token<PipeTransform>>): this;
19
22
  useGlobalGuards(...guards: GuardType[]): this;
23
+ useGlobalGuardTokens(...guardTokens: Array<Token<CanActivate>>): this;
20
24
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
25
+ useGlobalInterceptorTokens(...interceptorTokens: Array<Token<NestInterceptor>>): this;
21
26
  useGlobalFilters(...filters: FilterType[]): this;
27
+ useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
22
28
  private instantiate;
29
+ private instantiateMany;
30
+ private getRequestContainer;
23
31
  registerController(controller: Type): this;
24
32
  build(): Promise<Hono>;
25
33
  private createExecutionContext;
@@ -35,43 +35,68 @@ export class RouteManager {
35
35
  return this;
36
36
  }
37
37
  useGlobalMiddleware(...middleware) {
38
- for (const mw of middleware){
39
- this.globalMiddleware.push(this.instantiate(mw));
40
- }
38
+ this.globalMiddleware.push(...middleware);
39
+ return this;
40
+ }
41
+ useGlobalMiddlewareTokens(...middlewareTokens) {
42
+ this.globalMiddleware.push(...middlewareTokens);
41
43
  return this;
42
44
  }
43
45
  useGlobalPipes(...pipes) {
44
- for (const pipe of pipes){
45
- this.globalPipes.push(this.instantiate(pipe));
46
- }
46
+ this.globalPipes.push(...pipes);
47
+ return this;
48
+ }
49
+ useGlobalPipeTokens(...pipeTokens) {
50
+ this.globalPipes.push(...pipeTokens);
47
51
  return this;
48
52
  }
49
53
  useGlobalGuards(...guards) {
50
- for (const guard of guards){
51
- this.globalGuards.push(this.instantiate(guard));
52
- }
54
+ this.globalGuards.push(...guards);
55
+ return this;
56
+ }
57
+ useGlobalGuardTokens(...guardTokens) {
58
+ this.globalGuards.push(...guardTokens);
53
59
  return this;
54
60
  }
55
61
  useGlobalInterceptors(...interceptors) {
56
- for (const interceptor of interceptors){
57
- this.globalInterceptors.push(this.instantiate(interceptor));
58
- }
62
+ this.globalInterceptors.push(...interceptors);
63
+ return this;
64
+ }
65
+ useGlobalInterceptorTokens(...interceptorTokens) {
66
+ this.globalInterceptors.push(...interceptorTokens);
59
67
  return this;
60
68
  }
61
69
  useGlobalFilters(...filters) {
62
- for (const filter of filters){
63
- this.globalFilters.push(this.instantiate(filter));
64
- }
70
+ this.globalFilters.push(...filters);
71
+ return this;
72
+ }
73
+ useGlobalFilterTokens(...filterTokens) {
74
+ this.globalFilters.push(...filterTokens);
65
75
  return this;
66
76
  }
67
- instantiate(classOrInstance) {
68
- if (typeof classOrInstance !== 'function') {
69
- return classOrInstance;
77
+ instantiate(classOrInstance, container) {
78
+ if (typeof classOrInstance === 'function') {
79
+ if (container.has(classOrInstance)) {
80
+ return container.resolve(classOrInstance);
81
+ }
82
+ return new classOrInstance();
83
+ }
84
+ if (container.has(classOrInstance)) {
85
+ return container.resolve(classOrInstance);
70
86
  }
71
- if (this.container.has(classOrInstance)) {
72
- return this.container.resolve(classOrInstance);
87
+ return classOrInstance;
88
+ }
89
+ instantiateMany(items, container) {
90
+ return items.map((item)=>this.instantiate(item, container));
91
+ }
92
+ getRequestContainer(c) {
93
+ const existing = c.get('container');
94
+ if (existing) {
95
+ return existing;
73
96
  }
74
- return new classOrInstance();
97
+ const child = this.container.createChild();
98
+ c.set('container', child);
99
+ return child;
75
100
  }
76
101
  registerController(controller) {
77
102
  const prefix = MetadataRegistry.getControllerPath(controller);
@@ -94,12 +119,15 @@ export class RouteManager {
94
119
  const app = new Hono();
95
120
  // Register global middleware on the Hono app
96
121
  for (const mw of this.globalMiddleware){
97
- app.use('*', (c, next)=>mw.use(c, next));
122
+ app.use('*', (c, next)=>{
123
+ const requestContainer = this.getRequestContainer(c);
124
+ const resolved = this.instantiate(mw, requestContainer);
125
+ return resolved.use(c, next);
126
+ });
98
127
  }
99
128
  // First pass: register all custom routes (must come before CRUD /:id routes)
100
129
  for (const { controller, metadata, routes } of this.controllers){
101
130
  if (routes.length > 0) {
102
- const instance = this.container.resolve(controller);
103
131
  const allParamMetadata = MetadataRegistry.getParameters(controller);
104
132
  for (const route of routes){
105
133
  // Determine effective version: route-level overrides controller-level
@@ -108,11 +136,14 @@ export class RouteManager {
108
136
  const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
109
137
  // Register per-route middleware as Hono middleware before the handler
110
138
  const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
111
- const resolvedMw = ComponentManager.resolveMiddleware(middlewareItems);
112
- const handler = this.createHandler(instance, route, controller, allParamMetadata);
139
+ const handler = this.createHandler(route, controller, allParamMetadata);
113
140
  for (const fullPath of versionedPaths){
114
- for (const mw of resolvedMw){
115
- app.use(fullPath, (c, next)=>mw.use(c, next));
141
+ for (const middlewareItem of middlewareItems){
142
+ app.use(fullPath, (c, next)=>{
143
+ const requestContainer = this.getRequestContainer(c);
144
+ const resolved = this.instantiate(middlewareItem, requestContainer);
145
+ return resolved.use(c, next);
146
+ });
116
147
  }
117
148
  this.registerRoute(app, route.method, fullPath, handler);
118
149
  }
@@ -128,7 +159,7 @@ export class RouteManager {
128
159
  const { buildCrudRoutes } = await import(pkg);
129
160
  await buildCrudRoutes(app, controller, metadata.prefix, crudConfig, {
130
161
  globalPrefix: this.globalPrefix,
131
- globalGuards: this.globalGuards,
162
+ globalGuards: this.instantiateMany(this.globalGuards, this.container),
132
163
  joinPaths: this.joinPaths.bind(this)
133
164
  });
134
165
  } catch {
@@ -146,51 +177,46 @@ export class RouteManager {
146
177
  getRequest: ()=>c.req.raw
147
178
  };
148
179
  }
149
- createHandler(instance, route, controller, allParamMetadata) {
180
+ createHandler(route, controller, allParamMetadata) {
150
181
  const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
151
182
  // Read param types once at build time for metatype population
152
- const paramTypes = Reflect.getMetadata('design:paramtypes', instance.constructor.prototype, route.handlerName);
153
- // Pre-resolve controller + method level components at build time
183
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
184
+ // Collect controller + method level components at build time
154
185
  const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
155
186
  const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
156
187
  const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
157
- const methodFilters = ComponentManager.getComponents('filter', controller, route.handlerName);
158
- const resolvedMethodGuards = ComponentManager.resolveGuards(methodGuards);
159
- const resolvedMethodPipes = ComponentManager.resolvePipes(methodPipes);
160
- const resolvedMethodInterceptors = ComponentManager.resolveInterceptors(methodInterceptors);
161
188
  // Filters: reverse order (handler → controller → global) — closest to handler runs first
162
- const resolvedMethodFilters = ComponentManager.resolveFilters([
163
- ...methodFilters
164
- ].reverse());
189
+ const methodFilters = [
190
+ ...ComponentManager.getComponents('filter', controller, route.handlerName)
191
+ ].reverse();
165
192
  // Read response decorators at build time
166
193
  const httpCode = getHttpCode(controller, route.handlerName);
167
194
  const responseHeaders = getResponseHeaders(controller, route.handlerName);
168
195
  const redirect = getRedirect(controller, route.handlerName);
169
196
  return async (c)=>{
170
- // Create a child container for request-scoped providers
171
- const requestContainer = this.container.createChild();
172
- c.set('container', requestContainer);
173
197
  // Combine global + method at request time (allows post-create registration)
198
+ const requestContainer = this.getRequestContainer(c);
174
199
  const guards = [
175
- ...this.globalGuards,
176
- ...resolvedMethodGuards
200
+ ...this.instantiateMany(this.globalGuards, requestContainer),
201
+ ...this.instantiateMany(methodGuards, requestContainer)
177
202
  ];
178
203
  const pipes = [
179
- ...this.globalPipes,
180
- ...resolvedMethodPipes
204
+ ...this.instantiateMany(this.globalPipes, requestContainer),
205
+ ...this.instantiateMany(methodPipes, requestContainer)
181
206
  ];
182
207
  const interceptors = [
183
- ...this.globalInterceptors,
184
- ...resolvedMethodInterceptors
208
+ ...this.instantiateMany(this.globalInterceptors, requestContainer),
209
+ ...this.instantiateMany(methodInterceptors, requestContainer)
185
210
  ];
186
211
  const filters = [
187
- ...resolvedMethodFilters,
188
- ...this.globalFilters
212
+ ...this.instantiateMany(methodFilters, requestContainer),
213
+ ...this.instantiateMany(this.globalFilters, requestContainer)
189
214
  ];
190
215
  const executionContext = this.createExecutionContext(c, controller, route);
191
216
  try {
217
+ const instance = requestContainer.resolve(controller);
192
218
  // 1. Extract args + run pipes
193
- const args = await this.extractArguments(c, paramMetadata, pipes, paramTypes);
219
+ const args = await this.extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes);
194
220
  // 2. Guards (fail-fast)
195
221
  for (const guard of guards){
196
222
  const canActivate = await guard.canActivate(executionContext);
@@ -247,7 +273,7 @@ export class RouteManager {
247
273
  }
248
274
  };
249
275
  }
250
- async extractArguments(c, paramMetadata, pipes, paramTypes) {
276
+ async extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes) {
251
277
  if (paramMetadata.length === 0) {
252
278
  return [
253
279
  c
@@ -269,7 +295,7 @@ export class RouteManager {
269
295
  // Run param-level pipes
270
296
  if (param.pipes && param.pipes.length > 0) {
271
297
  for (const paramPipe of param.pipes){
272
- const pipeInstance = this.instantiate(paramPipe);
298
+ const pipeInstance = this.instantiate(paramPipe, requestContainer);
273
299
  value = await pipeInstance.transform(value, metadata);
274
300
  }
275
301
  }
@@ -9,12 +9,16 @@ export declare class ModuleLoader {
9
9
  private collectedControllers;
10
10
  private registeredProviders;
11
11
  private moduleExportsCache;
12
+ private appProviderCounter;
13
+ private appProviderTokens;
12
14
  constructor(container: Container, router: RouteManager);
13
15
  load(rootModule: Type): void;
14
16
  private processModule;
15
17
  private registerProvider;
18
+ private isAppToken;
16
19
  private buildExportSet;
17
20
  getControllers(): Type[];
18
21
  getRegisteredProviders(): Token[];
22
+ getAppProviderTokens(token: Token): Token[];
19
23
  resolveAllInstances(): unknown[];
20
24
  }
@@ -1,3 +1,6 @@
1
+ import { Scope } from "../constants.js";
2
+ import { InjectionToken } from "../container/types.js";
3
+ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
1
4
  import { getModuleMetadata, isModule } from "./decorators.js";
2
5
  function isDynamicModule(value) {
3
6
  return typeof value === 'object' && value !== null && 'module' in value && typeof value.module === 'function';
@@ -10,6 +13,29 @@ export class ModuleLoader {
10
13
  collectedControllers = [];
11
14
  registeredProviders = [];
12
15
  moduleExportsCache = new Map();
16
+ appProviderCounter = 0;
17
+ appProviderTokens = new Map([
18
+ [
19
+ APP_GUARD,
20
+ []
21
+ ],
22
+ [
23
+ APP_PIPE,
24
+ []
25
+ ],
26
+ [
27
+ APP_INTERCEPTOR,
28
+ []
29
+ ],
30
+ [
31
+ APP_FILTER,
32
+ []
33
+ ],
34
+ [
35
+ APP_MIDDLEWARE,
36
+ []
37
+ ]
38
+ ]);
13
39
  constructor(container, router){
14
40
  this.container = container;
15
41
  this.router = router;
@@ -98,14 +124,29 @@ export class ModuleLoader {
98
124
  }
99
125
  } else {
100
126
  const token = provider.token;
101
- if (token && !this.container.has(token)) {
127
+ if (!token) {
102
128
  this.container.register(provider);
103
- this.registeredProviders.push(token);
104
- } else if (!token) {
129
+ return;
130
+ }
131
+ if (this.isAppToken(token)) {
132
+ const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
133
+ this.container.register({
134
+ ...provider,
135
+ token: syntheticToken
136
+ });
137
+ this.registeredProviders.push(syntheticToken);
138
+ this.appProviderTokens.get(token).push(syntheticToken);
139
+ return;
140
+ }
141
+ if (!this.container.has(token)) {
105
142
  this.container.register(provider);
143
+ this.registeredProviders.push(token);
106
144
  }
107
145
  }
108
146
  }
147
+ isAppToken(token) {
148
+ return token === APP_GUARD || token === APP_PIPE || token === APP_INTERCEPTOR || token === APP_FILTER || token === APP_MIDDLEWARE;
149
+ }
109
150
  buildExportSet(exports, providers, importedProviders) {
110
151
  const exportSet = new Set();
111
152
  for (const exported of exports){
@@ -134,10 +175,18 @@ export class ModuleLoader {
134
175
  ...this.registeredProviders
135
176
  ];
136
177
  }
178
+ getAppProviderTokens(token) {
179
+ return [
180
+ ...this.appProviderTokens.get(token) ?? []
181
+ ];
182
+ }
137
183
  resolveAllInstances() {
138
184
  const instances = [];
139
185
  for (const token of this.registeredProviders){
140
186
  try {
187
+ if (this.container.getProviderScope(token) === Scope.REQUEST) {
188
+ continue;
189
+ }
141
190
  const instance = this.container.resolve(token);
142
191
  instances.push(instance);
143
192
  } catch {
@@ -146,6 +195,9 @@ export class ModuleLoader {
146
195
  }
147
196
  for (const controller of this.collectedControllers){
148
197
  try {
198
+ if (this.container.getProviderScope(controller) === Scope.REQUEST) {
199
+ continue;
200
+ }
149
201
  const instance = this.container.resolve(controller);
150
202
  if (!instances.includes(instance)) {
151
203
  instances.push(instance);
@@ -2,10 +2,19 @@ import type { OnApplicationBootstrap, OnModuleDestroy } from '../lifecycle/index
2
2
  import { ScheduleRegistry } from './schedule.registry';
3
3
  export declare class ScheduleExecutor implements OnApplicationBootstrap, OnModuleDestroy {
4
4
  private registry;
5
- private timers;
5
+ private intervalTimers;
6
+ private cronTimers;
7
+ private cronMatcherCache;
8
+ private lastCronMinute;
6
9
  private running;
7
10
  constructor(registry: ScheduleRegistry);
8
11
  onApplicationBootstrap(): void;
9
- private scheduleNext;
12
+ private scheduleInterval;
13
+ private scheduleCron;
14
+ private invoke;
15
+ private getCronMatcher;
16
+ private parseField;
17
+ private parseRange;
18
+ private parseCronNumber;
10
19
  onModuleDestroy(): void;
11
20
  }
@@ -11,38 +11,153 @@ import { Injectable } from "../container/index.js";
11
11
  import { ScheduleRegistry } from "./schedule.registry.js";
12
12
  export class ScheduleExecutor {
13
13
  registry;
14
- timers = [];
14
+ intervalTimers = [];
15
+ cronTimers = [];
16
+ cronMatcherCache = new Map();
17
+ lastCronMinute = new Map();
15
18
  running = true;
16
19
  constructor(registry){
17
20
  this.registry = registry;
18
21
  }
19
22
  onApplicationBootstrap() {
20
- const jobs = this.registry.getIntervalJobs();
21
- for (const job of jobs){
22
- this.scheduleNext(job.instance, job.methodName, job.ms);
23
+ const intervalJobs = this.registry.getIntervalJobs();
24
+ const cronJobs = this.registry.getCronJobs();
25
+ for (const job of intervalJobs){
26
+ this.scheduleInterval(job.instance, job.methodName, job.ms);
27
+ }
28
+ for(let i = 0; i < cronJobs.length; i++){
29
+ const job = cronJobs[i];
30
+ this.scheduleCron(job.instance, job.methodName, job.expression, i);
23
31
  }
24
32
  }
25
- scheduleNext(instance, methodName, ms) {
33
+ scheduleInterval(instance, methodName, ms) {
26
34
  if (!this.running) return;
27
- const timer = setTimeout(async ()=>{
28
- try {
29
- const method = instance[methodName];
30
- if (typeof method === 'function') {
31
- await method.call(instance);
32
- }
33
- } catch {
34
- // Swallow errors to keep the loop alive
35
- }
36
- this.scheduleNext(instance, methodName, ms);
35
+ const timer = setInterval(()=>{
36
+ void this.invoke(instance, methodName);
37
37
  }, ms);
38
- this.timers.push(timer);
38
+ this.intervalTimers.push(timer);
39
+ }
40
+ scheduleCron(instance, methodName, expression, index) {
41
+ if (!this.running) return;
42
+ const matcher = this.getCronMatcher(expression);
43
+ if (!matcher) {
44
+ return;
45
+ }
46
+ const jobKey = `${index}:${methodName}:${expression}`;
47
+ const timer = setInterval(()=>{
48
+ const now = new Date();
49
+ const minuteKey = Math.floor(now.getTime() / 60_000);
50
+ if (this.lastCronMinute.get(jobKey) === minuteKey) {
51
+ return;
52
+ }
53
+ if (!matcher(now)) {
54
+ return;
55
+ }
56
+ this.lastCronMinute.set(jobKey, minuteKey);
57
+ void this.invoke(instance, methodName);
58
+ }, 1000);
59
+ this.cronTimers.push(timer);
60
+ }
61
+ async invoke(instance, methodName) {
62
+ try {
63
+ const method = instance[methodName];
64
+ if (typeof method === 'function') {
65
+ await method.call(instance);
66
+ }
67
+ } catch {
68
+ // Swallow errors to keep the scheduler alive
69
+ }
70
+ }
71
+ getCronMatcher(expression) {
72
+ if (this.cronMatcherCache.has(expression)) {
73
+ return this.cronMatcherCache.get(expression) ?? null;
74
+ }
75
+ const fields = expression.trim().split(/\s+/);
76
+ if (fields.length !== 5) {
77
+ this.cronMatcherCache.set(expression, null);
78
+ return null;
79
+ }
80
+ const [minuteField, hourField, dayField, monthField, weekdayField] = fields;
81
+ const minuteMatcher = this.parseField(minuteField, 0, 59);
82
+ const hourMatcher = this.parseField(hourField, 0, 23);
83
+ const dayMatcher = this.parseField(dayField, 1, 31);
84
+ const monthMatcher = this.parseField(monthField, 1, 12);
85
+ const weekdayMatcher = this.parseField(weekdayField, 0, 7);
86
+ if (!minuteMatcher || !hourMatcher || !dayMatcher || !monthMatcher || !weekdayMatcher) {
87
+ this.cronMatcherCache.set(expression, null);
88
+ return null;
89
+ }
90
+ const matcher = (date)=>{
91
+ const weekday = date.getDay();
92
+ return minuteMatcher(date.getMinutes()) && hourMatcher(date.getHours()) && dayMatcher(date.getDate()) && monthMatcher(date.getMonth() + 1) && weekdayMatcher(weekday);
93
+ };
94
+ this.cronMatcherCache.set(expression, matcher);
95
+ return matcher;
96
+ }
97
+ parseField(field, min, max) {
98
+ const segments = field.split(',');
99
+ const predicates = [];
100
+ for (const rawSegment of segments){
101
+ const segment = rawSegment.trim();
102
+ if (!segment) return null;
103
+ const stepParts = segment.split('/');
104
+ if (stepParts.length > 2) return null;
105
+ const baseSegment = stepParts[0];
106
+ const step = stepParts.length === 2 ? Number(stepParts[1]) : 1;
107
+ if (!Number.isInteger(step) || step <= 0) return null;
108
+ const range = this.parseRange(baseSegment, min, max);
109
+ if (!range) return null;
110
+ predicates.push((value)=>{
111
+ if (value < range.start || value > range.end) return false;
112
+ return (value - range.start) % step === 0;
113
+ });
114
+ }
115
+ return (value)=>predicates.some((predicate)=>predicate(value));
116
+ }
117
+ parseRange(segment, min, max) {
118
+ if (segment === '*') {
119
+ return {
120
+ start: min,
121
+ end: max
122
+ };
123
+ }
124
+ const bounds = segment.split('-');
125
+ if (bounds.length === 1) {
126
+ const value = this.parseCronNumber(bounds[0], min, max);
127
+ if (value === null) return null;
128
+ return {
129
+ start: value,
130
+ end: value
131
+ };
132
+ }
133
+ if (bounds.length !== 2) return null;
134
+ const start = this.parseCronNumber(bounds[0], min, max);
135
+ const end = this.parseCronNumber(bounds[1], min, max);
136
+ if (start === null || end === null || start > end) return null;
137
+ return {
138
+ start,
139
+ end
140
+ };
141
+ }
142
+ parseCronNumber(raw, min, max) {
143
+ const value = Number(raw);
144
+ if (!Number.isInteger(value)) return null;
145
+ // Cron allows both 0 and 7 for Sunday.
146
+ const normalized = max === 7 && value === 7 ? 0 : value;
147
+ if (normalized < min || normalized > max) return null;
148
+ return normalized;
39
149
  }
40
150
  onModuleDestroy() {
41
151
  this.running = false;
42
- for (const timer of this.timers){
43
- clearTimeout(timer);
152
+ for (const timer of this.intervalTimers){
153
+ clearInterval(timer);
154
+ }
155
+ for (const timer of this.cronTimers){
156
+ clearInterval(timer);
44
157
  }
45
- this.timers = [];
158
+ this.intervalTimers = [];
159
+ this.cronTimers = [];
160
+ this.lastCronMinute.clear();
46
161
  }
47
162
  }
48
163
  ScheduleExecutor = _ts_decorate([
@@ -30,7 +30,7 @@ export class OverrideBy {
30
30
  token: this.token,
31
31
  provider: {
32
32
  token: this.token,
33
- useFactory: ()=>new cls()
33
+ useClass: cls
34
34
  }
35
35
  });
36
36
  return this.builder;
@@ -102,21 +102,35 @@ export class TestingModuleBuilder {
102
102
  ComponentManager.init(container);
103
103
  const loader = new ModuleLoader(container, routeManager);
104
104
  loader.load(TestRootModule);
105
- // Resolve APP_* tokens
106
- if (container.has(APP_GUARD)) {
107
- routeManager.useGlobalGuards(container.resolve(APP_GUARD));
105
+ const appGuards = loader.getAppProviderTokens(APP_GUARD);
106
+ if (appGuards.length > 0) {
107
+ routeManager.useGlobalGuardTokens(...appGuards);
108
+ } else if (container.has(APP_GUARD)) {
109
+ routeManager.useGlobalGuardTokens(APP_GUARD);
108
110
  }
109
- if (container.has(APP_PIPE)) {
110
- routeManager.useGlobalPipes(container.resolve(APP_PIPE));
111
+ const appPipes = loader.getAppProviderTokens(APP_PIPE);
112
+ if (appPipes.length > 0) {
113
+ routeManager.useGlobalPipeTokens(...appPipes);
114
+ } else if (container.has(APP_PIPE)) {
115
+ routeManager.useGlobalPipeTokens(APP_PIPE);
111
116
  }
112
- if (container.has(APP_INTERCEPTOR)) {
113
- routeManager.useGlobalInterceptors(container.resolve(APP_INTERCEPTOR));
117
+ const appInterceptors = loader.getAppProviderTokens(APP_INTERCEPTOR);
118
+ if (appInterceptors.length > 0) {
119
+ routeManager.useGlobalInterceptorTokens(...appInterceptors);
120
+ } else if (container.has(APP_INTERCEPTOR)) {
121
+ routeManager.useGlobalInterceptorTokens(APP_INTERCEPTOR);
114
122
  }
115
- if (container.has(APP_FILTER)) {
116
- routeManager.useGlobalFilters(container.resolve(APP_FILTER));
123
+ const appFilters = loader.getAppProviderTokens(APP_FILTER);
124
+ if (appFilters.length > 0) {
125
+ routeManager.useGlobalFilterTokens(...appFilters);
126
+ } else if (container.has(APP_FILTER)) {
127
+ routeManager.useGlobalFilterTokens(APP_FILTER);
117
128
  }
118
- if (container.has(APP_MIDDLEWARE)) {
119
- routeManager.useGlobalMiddleware(container.resolve(APP_MIDDLEWARE));
129
+ const appMiddleware = loader.getAppProviderTokens(APP_MIDDLEWARE);
130
+ if (appMiddleware.length > 0) {
131
+ routeManager.useGlobalMiddlewareTokens(...appMiddleware);
132
+ } else if (container.has(APP_MIDDLEWARE)) {
133
+ routeManager.useGlobalMiddlewareTokens(APP_MIDDLEWARE);
120
134
  }
121
135
  const app = new VelaApplication(container, routeManager);
122
136
  const instances = loader.resolveAllInstances();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",