diff --git a/integrations/sample-app/app/boot/container.ts b/integrations/sample-app/app/boot/container.ts index 2e8a3aa..d5334c6 100644 --- a/integrations/sample-app/app/boot/container.ts +++ b/integrations/sample-app/app/boot/container.ts @@ -2,7 +2,6 @@ import config from '#config/index'; import { IntentAppContainer, IntentProvidersFactory } from '@intentjs/core'; import { AppServiceProvider } from '#boot/sp/app'; import { ConsoleServiceProvider } from '#boot/sp/console'; - export class ApplicationContainer extends IntentAppContainer { build() { /** @@ -16,10 +15,6 @@ export class ApplicationContainer extends IntentAppContainer { * Register our main application service providers. */ this.add(AppServiceProvider); - - /** - * Registering our console commands service providers. - */ this.add(ConsoleServiceProvider); } } diff --git a/integrations/sample-app/app/boot/sp/app.ts b/integrations/sample-app/app/boot/sp/app.ts index 1dd9659..1e67d84 100644 --- a/integrations/sample-app/app/boot/sp/app.ts +++ b/integrations/sample-app/app/boot/sp/app.ts @@ -1,5 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { IntentApplicationContext, ServiceProvider } from '@intentjs/core'; +import { + IntentApplicationContext, + ModuleRef, + ServiceProvider, +} from '@intentjs/core'; import { OrderPlacedListener } from '#events/listeners/sample-listener'; import { QueueJobs } from '#jobs/job'; import { UserDbRepository } from '#repositories/userDbRepository'; @@ -38,71 +42,15 @@ export class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application service here. */ - boot(app: IntentApplicationContext) { - /** - * Schedule Intent Command to run daily. - */ - - Schedule.exec('ls -la') - .everyTwoSeconds() - .appendOutputToFile('output.txt') - .emailOutputTo('vinayak@tryhanalabs.com') - .run(); - - // Schedule.command('send:email') - // // .days([Schedule.MONDAY, Schedule.THURSDAY]) - // .hourly() - // .timezone('America/Chicago') - // .between('8:00', '17:00') - // .run(); - - /** - * Simple callback, with lifecycle methods `before` and `after`. - */ - // Schedule.call(() => { - // console.log('inside the schedule method'); - // return 'hello'; - // }) - // .purpose('sample scheduler') - // .before(() => console.log('this will run before the cron')) - // .after((output: any) => - // console.log('this will run after the cron', output), - // ) - // .onSuccess((result) => - // console.log('this will run on success the cron', result), - // ) - // .onFailure((error) => - // console.log('this will run on failure the cron', error), - // ) - // // .pingBefore('https://webhook.site/79dcb789-869b-459d-9ba9-638aae449328') - // .thenPing('https://webhook.site/79dcb789-869b-459d-9ba9-638aae449328') - // .weekends() - // .everyTwoSeconds() - // // .pingOnSuccess('https://webhook.site/79dcb789-869b-459d-9ba9-638aae449328') - // .when(() => true) - // .appendOutputToFile('output.txt') - // .run(); + boot(app: IntentApplicationContext) {} - /** - * Running a job every day at 5AM. - */ - // Schedule.job({ - // job: 'process_abandoned_cart', - // data: { from: '2024-04-16', to: '2024-04-17' }, - // }) - // .purpose('cron dispatching job every day at 5AM') - // .everyFiveSeconds() - // .weekends() - // .run(); + /** + * Shutdown any application service here. + */ + shutdown(app: IntentApplicationContext) {} - // Schedule.command('emails:send') - // .daily() - // .onSuccess((result) => { - // console.log('emails:send on success', result); - // }) - // .onFailure((error: Error) => { - // console.log('emails:send on failure', error); - // }) - // .run(); - } + /** + * Register any schedules here. + */ + async schedules(ref: ModuleRef): Promise {} } diff --git a/integrations/sample-app/app/boot/sp/console.ts b/integrations/sample-app/app/boot/sp/console.ts index 4704fa5..478648e 100644 --- a/integrations/sample-app/app/boot/sp/console.ts +++ b/integrations/sample-app/app/boot/sp/console.ts @@ -1,36 +1,37 @@ -import { IntentApplicationContext, ServiceProvider } from '@intentjs/core'; +import { IntentApplicationContext } from '@intentjs/core'; import { TestCacheConsoleCommand } from '#console/cache'; import { GreetingCommand } from '#console/greeting'; import { TestLogConsoleCommand } from '#console/log'; import { TestMailConsoleCommand } from '#console/mailer'; import { TestQueueConsoleCommand } from '#console/queue'; import { TestStorageConsoleCommand } from '#console/storage'; -import { Dispatch } from '@intentjs/core/queue'; +import { ModuleRef, ServiceProvider } from '@intentjs/core'; +import { Schedule } from '@intentjs/core/schedule'; export class ConsoleServiceProvider extends ServiceProvider { - /** - * Register any application services here. - */ register() { - this.bind(GreetingCommand, TestCacheConsoleCommand); - this.bind(TestStorageConsoleCommand); - this.bind(TestLogConsoleCommand); - this.bind(TestQueueConsoleCommand); - this.bind(TestMailConsoleCommand); + this.bind( + TestCacheConsoleCommand, + GreetingCommand, + TestLogConsoleCommand, + TestQueueConsoleCommand, + TestMailConsoleCommand, + TestStorageConsoleCommand, + ); } /** - * Bootstrap any application service here. - * + * Bootstrap any application services here. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - boot(app: IntentApplicationContext) { - let i = 0; - setInterval(async () => { - await Dispatch({ - job: 'redis_job', - data: { count: i++ }, - }); - }, 1000); - } + boot(app: IntentApplicationContext) {} + + /** + * Shutdown any application services here. + */ + shutdown(app: IntentApplicationContext) {} + + /** + * Register any schedules here. + */ + async schedules(ref: ModuleRef): Promise {} } diff --git a/integrations/sample-app/config/app.ts b/integrations/sample-app/config/app.ts index 4db678b..fc5c80c 100644 --- a/integrations/sample-app/config/app.ts +++ b/integrations/sample-app/config/app.ts @@ -98,5 +98,10 @@ export default configNamespace( profilesSampleRate: 1.0, integrateNodeProfile: true, }, + + schedules: { + runInAnotherThread: true, + timezone: 'Asia/Kolkata', + }, }), ); diff --git a/integrations/sample-app/config/database.ts b/integrations/sample-app/config/database.ts index f35a546..8c1397e 100644 --- a/integrations/sample-app/config/database.ts +++ b/integrations/sample-app/config/database.ts @@ -5,7 +5,7 @@ import { knexSnakeCaseMappers } from 'objection'; export default configNamespace( 'db', (): DatabaseOptions => ({ - isGlobal: true, + isGlobal: false, default: 'pg', connections: { pg: { diff --git a/packages/core/lib/application/interface.ts b/packages/core/lib/application/interface.ts index 2a585de..a5b6088 100644 --- a/packages/core/lib/application/interface.ts +++ b/packages/core/lib/application/interface.ts @@ -18,4 +18,9 @@ export interface AppConfig { validationErrorSerializer?: GenericClass; }; sentry?: SentryConfig; + + schedules?: { + runInAnotherThread?: boolean; + timezone?: string; + }; } diff --git a/packages/core/lib/foundation/actuator.ts b/packages/core/lib/foundation/actuator.ts index 544926b..87169a4 100644 --- a/packages/core/lib/foundation/actuator.ts +++ b/packages/core/lib/foundation/actuator.ts @@ -1,5 +1,5 @@ import { IntentHttpServer } from '../rest/foundation/server.js'; -import { IntentProcess } from './intent-process.js'; +import { IntentConsoleProcess } from './intent-process.js'; type ContainerImporterType = () => any; @@ -22,8 +22,8 @@ export class Actuator { * Get the intent process * @returns Get the intent process */ - cli(): IntentProcess { - return new IntentProcess(this); + cli(): IntentConsoleProcess { + return new IntentConsoleProcess(this); } http(): IntentHttpServer { diff --git a/packages/core/lib/foundation/app-container.ts b/packages/core/lib/foundation/app-container.ts index 07669d5..f993e0d 100644 --- a/packages/core/lib/foundation/app-container.ts +++ b/packages/core/lib/foundation/app-container.ts @@ -1,6 +1,8 @@ import { Provider } from '@nestjs/common'; import { IntentApplicationContext, Type } from '../interfaces/index.js'; import { ImportType, ServiceProvider } from './service-provider.js'; +import { ModuleRef } from '@nestjs/core'; +import { SchedulerRegistry } from '../scheduler/metadata.js'; export abstract class IntentAppContainer { static serviceProviders: ServiceProvider[] = []; @@ -33,7 +35,14 @@ export abstract class IntentAppContainer { async boot(app: IntentApplicationContext): Promise { for (const serviceProvider of IntentAppContainer.serviceProviders) { - serviceProvider.boot(app); + await serviceProvider.boot(app); + await serviceProvider.schedules(app.get(ModuleRef)); + } + } + + async shutdown(app: IntentApplicationContext): Promise { + for (const serviceProvider of IntentAppContainer.serviceProviders) { + await serviceProvider.shutdown(app); } } diff --git a/packages/core/lib/foundation/container-factory.ts b/packages/core/lib/foundation/container-factory.ts index 9b3bb0a..0076c1b 100644 --- a/packages/core/lib/foundation/container-factory.ts +++ b/packages/core/lib/foundation/container-factory.ts @@ -2,13 +2,19 @@ import { NestFactory } from '@nestjs/core'; import type { IntentApplicationContext, Type } from '../interfaces/index.js'; import { ModuleBuilder } from './module-builder.js'; import { IntentAppContainer } from './app-container.js'; +import { ServiceProvider } from './service-provider.js'; export class ContainerFactory { static async createStandalone( containerCls: Type, + extraServiceProviders: Type[] = [], ): Promise { const container = new containerCls(); + if (extraServiceProviders.length > 0) { + container.add(...extraServiceProviders); + } + container.build(); /** @@ -24,9 +30,9 @@ export class ContainerFactory { }); /** - * Run the `boot` method of the main application container + * Run the `boot` & `schedules` method of the main application container */ - container.boot(app); + await container.boot(app); return app; } diff --git a/packages/core/lib/foundation/index.ts b/packages/core/lib/foundation/index.ts index 618b42b..85a8c7c 100644 --- a/packages/core/lib/foundation/index.ts +++ b/packages/core/lib/foundation/index.ts @@ -5,3 +5,4 @@ export * from './container-factory.js'; export * from './decorators.js'; export * from './actuator.js'; export * from './intent-process.js'; +export { ModuleRef } from '@nestjs/core'; diff --git a/packages/core/lib/foundation/intent-process.ts b/packages/core/lib/foundation/intent-process.ts index 32d515c..b7a872a 100644 --- a/packages/core/lib/foundation/intent-process.ts +++ b/packages/core/lib/foundation/intent-process.ts @@ -5,7 +5,7 @@ import { Actuator } from './actuator.js'; import { ContainerFactory } from './container-factory.js'; import yargs from 'yargs-parser'; -export class IntentProcess { +export class IntentConsoleProcess { constructor(private readonly actuator: Actuator) {} async handle(args: string[]): Promise { diff --git a/packages/core/lib/foundation/service-provider.ts b/packages/core/lib/foundation/service-provider.ts index bff3bc7..f9790c9 100644 --- a/packages/core/lib/foundation/service-provider.ts +++ b/packages/core/lib/foundation/service-provider.ts @@ -6,6 +6,7 @@ import { Provider, } from '@nestjs/common'; import type { IntentApplicationContext, Type } from '../interfaces/index.js'; +import { ModuleRef } from '@nestjs/core'; export type ImportType = | Type @@ -13,33 +14,74 @@ export type ImportType = | Promise | ForwardReference; +/** + * Abstract base class for service providers. + * Service providers are used to register dependencies, configure the application, + * and define lifecycle hooks within the IntentJS framework. + */ export abstract class ServiceProvider { + /** + * Stores providers registered by this service provider. + */ private providers: Provider[] = []; + /** + * Stores modules imported by this service provider. + */ private imports: ImportType[] = []; + /** + * Retrieves all modules imported by this service provider. + * @returns An array of imported modules. + */ getAllImports(): ImportType[] { return this.imports; } + /** + * Retrieves all providers registered by this service provider. + * @returns An array of registered providers. + */ getAllProviders(): Provider[] { return this.providers; } + /** + * Adds modules to be imported. + * @param imports - The modules to import. + * @returns The current ServiceProvider instance. + */ import(...imports: ImportType[]): this { this.imports.push(...imports); return this; } + /** + * Registers one or more providers. + * @param cls - The provider(s) to register. + * @returns The current ServiceProvider instance. + */ bind(...cls: Provider[]): this { this.providers.push(...cls); return this; } + /** + * Binds a token to a specific value. + * @param token - The injection token. + * @param valueFn - The value to bind. + * @returns The current ServiceProvider instance. + */ bindWithValue(token: string | symbol | Type, valueFn: any): this { this.providers.push({ provide: token, useValue: valueFn }); return this; } + /** + * Binds a token to a specific class. + * @param token - The injection token. + * @param cls - The class to bind. + * @returns The current ServiceProvider instance. + */ bindWithClass(token: string | symbol | Type, cls: Type): this { this.providers.push({ provide: token, @@ -48,11 +90,24 @@ export abstract class ServiceProvider { return this; } + /** + * Binds a token to an existing instance identified by another token (alias). + * @param token - The injection token (alias). + * @param cls - The token of the existing instance. + * @returns The current ServiceProvider instance. + */ bindWithExisting(token: string, cls: Type): this { this.providers.push({ provide: token, useExisting: cls }); return this; } + /** + * Binds a token to a factory function. + * @param token - The injection token. + * @param factory - The factory function. + * @param inject - Optional dependencies to inject into the factory function. + * @returns The current ServiceProvider instance. + */ bindWithFactory( token: string | symbol | Type, factory: (...args: any[]) => T | Promise, @@ -62,12 +117,26 @@ export abstract class ServiceProvider { } /** - * Use this method to register any provider. + * Abstract method to be implemented by subclasses. + * Use this method to register providers and imports using the `bind*` and `import` methods. */ abstract register(); /** - * Use this method to run + * Abstract lifecycle hook executed during application bootstrapping. + * @param app - The Intent application context. */ abstract boot(app: IntentApplicationContext); + + /** + * Abstract lifecycle hook executed during application shutdown. + * @param app - The Intent application context. + */ + abstract shutdown(app: IntentApplicationContext); + + /** + * Abstract method for registering scheduled tasks or jobs. + * @param ref - The ModuleRef instance for resolving dependencies. + */ + abstract schedules(ref: ModuleRef): Promise; } diff --git a/packages/core/lib/scheduler/console/list.ts b/packages/core/lib/scheduler/console/list.ts index 1111f67..f945b3e 100644 --- a/packages/core/lib/scheduler/console/list.ts +++ b/packages/core/lib/scheduler/console/list.ts @@ -3,7 +3,9 @@ import { Command } from '../../console/decorators.js'; import { SchedulerRegistry } from '../metadata.js'; import pc from 'picocolors'; -@Command('schedule:list') +@Command('schedule:list', { + desc: 'Command to list all of the scheduled tasks', +}) export class ListScheduledTaskCommands { async handle(_cli: ConsoleIO): Promise { const schedules = SchedulerRegistry.getAllSchedules(); @@ -17,7 +19,6 @@ export class ListScheduledTaskCommands { ]); } - console.log(rows); _cli.table(['Schedule', 'Name', 'Purpose'], rows); return true; } diff --git a/packages/core/lib/scheduler/console/work.ts b/packages/core/lib/scheduler/console/work.ts new file mode 100644 index 0000000..51260a4 --- /dev/null +++ b/packages/core/lib/scheduler/console/work.ts @@ -0,0 +1,21 @@ +import { ConsoleIO } from '../../console/consoleIO.js'; +import { Command } from '../../console/decorators.js'; +import { SchedulerRegistry } from '../metadata.js'; + +@Command('schedule:work', { desc: 'Command to start the schedule worker' }) +export class ScheduleWorkerCommand { + async handle(_cli: ConsoleIO): Promise { + const schedules = SchedulerRegistry.getAllSchedules(); + _cli.info('Found ' + Object.keys(schedules).length + ' schedules'); + if (Object.keys(schedules).length === 0) { + _cli.error('No schedules found, exiting...'); + return true; + } + + for (const schedule of Object.values(schedules)) { + schedule.start(); + } + + return false; + } +} diff --git a/packages/core/lib/scheduler/options/interface.ts b/packages/core/lib/scheduler/options/interface.ts index 70e91cf..ebb5dbe 100644 --- a/packages/core/lib/scheduler/options/interface.ts +++ b/packages/core/lib/scheduler/options/interface.ts @@ -25,3 +25,7 @@ export type PingOptions = { url: string; ifCb: undefined | ((...args: any[]) => Promise | boolean); }; + +export type SchedulerConfig = { + autoStart?: boolean; +}; diff --git a/packages/core/lib/scheduler/schedule.ts b/packages/core/lib/scheduler/schedule.ts index 952f1d3..c60b347 100644 --- a/packages/core/lib/scheduler/schedule.ts +++ b/packages/core/lib/scheduler/schedule.ts @@ -14,6 +14,7 @@ import fs from 'fs-extra'; import execa from 'execa'; import { MailMessage } from '../mailer/message.js'; import { Mail } from '../mailer/mail.js'; +import { ConfigService } from '../config/service.js'; const { appendFileSync, writeFileSync } = fs; @@ -34,7 +35,7 @@ export class Schedule { private cronExpression: string; private beforeFunc: any; private afterFunc: any; - private autoStart: boolean; + private autoStart: boolean | undefined; private whenFunc: (...args: any[]) => Promise | boolean; private skipFunc: (...args: any[]) => Promise | boolean; private onSuccessFunc: (...args: any[]) => Promise | void; @@ -53,7 +54,6 @@ export class Schedule { this.cronJob = null; this.cronExpression = ''; this.handler = handler; - this.autoStart = true; this.frequency = new ScheduleFrequency(); this.pingBeforeOptions = { url: undefined, ifCb: undefined }; this.pingThenOptions = { url: undefined, ifCb: undefined }; @@ -102,11 +102,6 @@ export class Schedule { return this; } - noAutoStart(): this { - this.autoStart = false; - return this; - } - utcOffset(offset: string): this { return this; } @@ -363,11 +358,17 @@ export class Schedule { private makeCronJob() { this.scheduleName = this.scheduleName || `schedule_${ulid()}`; this.cronExpression = this.frequency.build(); + const autoStart = + ConfigService.get('app.schedules.runInAnotherThread') || false; + + const timezone = + this.tz ?? (ConfigService.get('app.schedules.timezone') as string); + this.cronJob = CronJob.from({ cronTime: this.cronExpression, onTick: this.composeHandler.bind(this), - start: this.autoStart, - timeZone: this.tz, + start: !autoStart, + timeZone: timezone, }); SchedulerRegistry.register(this.scheduleName, this); @@ -616,6 +617,10 @@ export class Schedule { this.cronJob.stop(); } + start() { + this.cronJob.start(); + } + lastExecution(): Date { return this.cronJob.lastExecution; } diff --git a/packages/core/lib/serviceProvider.ts b/packages/core/lib/serviceProvider.ts index 9709e86..3b6fff8 100644 --- a/packages/core/lib/serviceProvider.ts +++ b/packages/core/lib/serviceProvider.ts @@ -24,7 +24,8 @@ import { } from './config/index.js'; import { ReplConsole } from './repl/terminal.js'; import { ListScheduledTaskCommands } from './scheduler/console/list.js'; - +import { ScheduleWorkerCommand } from './scheduler/console/work.js'; +import { ModuleRef } from '@nestjs/core'; export const IntentProvidersFactory = ( config: any[], ): Type => { @@ -58,6 +59,7 @@ export const IntentProvidersFactory = ( BuildProjectCommand, DevServerCommand, ListScheduledTaskCommands, + ScheduleWorkerCommand, ); } @@ -65,5 +67,17 @@ export const IntentProvidersFactory = ( * Add your application boot logic here. */ boot() {} + + /** + * Add your application shutdown logic here. + */ + shutdown() {} + + /** + * Register any schedules here. + */ + schedules(ref: ModuleRef): Promise { + return Promise.resolve(); + } }; }; diff --git a/packages/core/package.json b/packages/core/package.json index 705c200..4b03a72 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@intentjs/core", - "version": "0.1.55", + "version": "0.1.59", "description": "Core module for Intent", "repository": { "type": "git",