@warlock.js/core 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/esm/application/application.d.mts +126 -4
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +133 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -1
- package/esm/cli/types.d.mts +2 -1
- package/esm/cli/types.d.mts.map +1 -1
- package/esm/config/config-getter.mjs +5 -5
- package/esm/config/config-getter.mjs.map +1 -1
- package/esm/config/config-loader.mjs +2 -2
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/access-connector.mjs +2 -2
- package/esm/connectors/access-connector.mjs.map +1 -1
- package/esm/connectors/cache-connector.mjs +2 -2
- package/esm/connectors/cache-connector.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +4 -1
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs +2 -2
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs +2 -2
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts +8 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +40 -7
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs +2 -2
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs +2 -2
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/notifications-connector.mjs +2 -2
- package/esm/connectors/notifications-connector.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +3 -3
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +7 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs +1 -1
- package/esm/http/config.mjs +2 -2
- package/esm/http/config.mjs.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -2
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/health.d.mts +57 -0
- package/esm/http/health.d.mts.map +1 -0
- package/esm/http/health.mjs +75 -0
- package/esm/http/health.mjs.map +1 -0
- package/esm/http/index.d.mts +2 -1
- package/esm/http/index.mjs +2 -1
- package/esm/http/middleware/idempotency.middleware.mjs +5 -5
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +2 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +4 -4
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/plugins.mjs +9 -9
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/response.mjs +5 -5
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts +16 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs +23 -3
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts +45 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +5 -4
- package/esm/index.mjs +6 -5
- package/esm/production/production-builder.mjs +2 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/utils/environment.d.mts +6 -1
- package/esm/utils/environment.d.mts.map +1 -1
- package/esm/utils/environment.mjs.map +1 -1
- package/esm/utils/index.d.mts +1 -1
- package/esm/utils/paths.mjs +2 -2
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +2 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +164 -2
- package/llms.txt +2 -1
- package/package.json +9 -9
- package/skills/health-checks/SKILL.md +98 -0
- package/skills/use-app-context/SKILL.md +62 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"production-builder.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/production/production-builder.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n putFileAsync,\r\n removeDirectoryAsync,\r\n} from \"@warlock.js/fs\";\r\nimport esbuild from \"esbuild\";\r\nimport glob from \"fast-glob\";\r\nimport path from \"path\";\r\nimport { tsconfigManager } from \"../dev-server/tsconfig-manager\";\r\nimport { appPath, warlockPath } from \"../utils\";\r\nimport { nativeNodeModulesPlugin } from \"./esbuild-plugins\";\r\nimport { resolveBuildConfig, type ResolvedBuildConfig } from \"./resolve-build-config\";\r\nimport { toCamelCase, toKebabCase } from \"@mongez/reinforcements\";\r\n\r\n/**\r\n * Production Builder\r\n * Generates production-ready files and bundles them for deployment\r\n * Build options are loaded from warlock.config.ts\r\n */\r\nexport class ProductionBuilder {\r\n private options!: ResolvedBuildConfig;\r\n private readonly productionDir = warlockPath(\"production\");\r\n\r\n /**\r\n * Main build entry point\r\n */\r\n public async build(): Promise<void> {\r\n console.log(colors.cyan(\"Building for production...\\n\"));\r\n\r\n // Step 1: Initialize options from config\r\n await this.initializeOptions();\r\n\r\n // Step 2: Generate combined files\r\n await this.generateCombinedFiles();\r\n\r\n // Step 3: Generate entry point\r\n await this.generateEntryPoint();\r\n\r\n // Step 4: Bundle with esbuild\r\n await this.bundle();\r\n\r\n // // Step 5: Remove production folder\r\n await removeDirectoryAsync(this.productionDir);\r\n\r\n console.log(colors.green(\"Build complete!\"));\r\n console.log(`Start production server by running ${colors.cyan(\"warlock start\")}`);\r\n }\r\n\r\n /**\r\n * Initialize options from warlock.config.ts\r\n */\r\n private async initializeOptions(): Promise<void> {\r\n this.options = resolveBuildConfig();\r\n\r\n // Ensure production directory exists\r\n await ensureDirectoryAsync(this.productionDir);\r\n }\r\n\r\n /**\r\n * Track which special files were generated\r\n */\r\n private generatedFiles = {\r\n locales: false,\r\n events: false,\r\n main: false,\r\n routes: false,\r\n };\r\n\r\n /**\r\n * Generate all combined files\r\n */\r\n private async generateCombinedFiles(): Promise<void> {\r\n console.log(colors.yellow(\" Generating production files...\"));\r\n\r\n // Generate bootstrap.ts file\r\n await this.generateBootstrap();\r\n\r\n // Generate config loader\r\n await this.generateConfigLoader();\r\n\r\n // Generate special files and track which ones have content\r\n const [locales, events, main, routes] = await Promise.all([\r\n this.generateLocales(),\r\n this.generateEvents(),\r\n this.generateMain(),\r\n this.generateRoutes(),\r\n ]);\r\n\r\n this.generatedFiles = { locales, events, main, routes };\r\n }\r\n\r\n /**\r\n * Generate bootstrap.ts - ensures bootstrap() runs first and sets production environment\r\n */\r\n private async generateBootstrap(): Promise<void> {\r\n let content = `import { bootstrap, Application } from \"@warlock.js/core\";\r\n\r\n// Set production environment\r\nApplication.setRuntimeStrategy(\"production\");\r\nApplication.setEnvironment(\"production\");\r\n\r\n// Bootstrap the application\r\nbootstrap();\r\n`;\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n content += \"import './../../src/app/bootstrap';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"bootstrap.ts\"), content);\r\n }\r\n\r\n /**\r\n * Glob for module files matching a pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModule(fileName: string): Promise<string[]> {\r\n const pattern = `**/${fileName}.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n // Convert to relative paths from .warlock/production/ to src/app/\r\n // e.g., \"users/main\" -> \"../../src/app/users/main\"\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Glob for files in a specific directory pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModuleDirectory(directory: string): Promise<string[]> {\r\n const pattern = `**/${directory}/*.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Generate config-loader.ts\r\n */\r\n private async generateConfigLoader(): Promise<void> {\r\n const configDirectory = path.join(process.cwd(), \"src/config\");\r\n\r\n const files = await glob(\"*.{ts,tsx}\", {\r\n cwd: configDirectory,\r\n absolute: false,\r\n });\r\n\r\n const configNames = files.map((f) => f.replace(/\\.(ts|tsx)$/, \"\"));\r\n\r\n const imports: string[] = [\r\n 'import config from \"@mongez/config\";',\r\n 'import { configSpecialHandlers } from \"@warlock.js/core\";',\r\n ];\r\n const configImports: string[] = [];\r\n const configSetCalls: string[] = [];\r\n const executors: string[] = [];\r\n\r\n for (const configName of configNames) {\r\n const properConfigName = toCamelCase(configName);\r\n const varName = `${properConfigName}Config`;\r\n configImports.push(`import ${varName} from \"../../src/config/${configName}\";`);\r\n configSetCalls.push(`config.set(\"${properConfigName}\", ${varName});`);\r\n executors.push(`await configSpecialHandlers.execute(\"${properConfigName}\", ${varName});`);\r\n }\r\n\r\n let content = [\r\n ...imports,\r\n \"\",\r\n \"// Config imports\",\r\n ...configImports,\r\n \"\",\r\n \"// Register configs\",\r\n ...configSetCalls,\r\n \"\",\r\n \"// Special handlers\",\r\n ...executors,\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n content += \"import './../../src/app/prestart';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"config-loader.ts\"), content);\r\n }\r\n\r\n /**\r\n * Generate locales.ts (only if there are locale files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateLocales(): Promise<boolean> {\r\n const files = await this.globModule(\"utils/locales\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"locales.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate events.ts (only if there are event files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateEvents(): Promise<boolean> {\r\n const files = await this.globModuleDirectory(\"events\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"events.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate main.ts (only if there are main files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateMain(): Promise<boolean> {\r\n const files = await this.globModule(\"main\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"main.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate routes.ts (only if there are route files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateRoutes(): Promise<boolean> {\r\n const files = await this.globModule(\"routes\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"routes.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate a file with imports from all given files\r\n */\r\n private async generateImportsFile(importPaths: string[], outputFile: string): Promise<void> {\r\n const imports = importPaths.map((importPath) => `import \"${importPath}\";`);\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, outputFile), content);\r\n }\r\n\r\n /**\r\n * Generate the main entry point (app.ts)\r\n */\r\n private async generateEntryPoint(): Promise<void> {\r\n console.log(colors.yellow(\" Generating entry point...\"));\r\n\r\n // Build imports based on which files were generated\r\n const imports: string[] = [\r\n \"// 1. Bootstrap (loads .env, initializes framework)\",\r\n 'import \"./bootstrap\";',\r\n \"\",\r\n \"// 2. Load configs\",\r\n 'import \"./config-loader\";',\r\n \"\",\r\n \"// 3. Start early-phase connectors (database, cache, logger, ...)\",\r\n \"// so data sources, cache, etc. are ready before app code runs\",\r\n 'import { connectorsManager, ConnectorLifecyclePhase } from \"@warlock.js/core\";',\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\",\r\n ];\r\n\r\n // App code uses dynamic `await import(...)` so each module's side\r\n // effects fire at THIS point in execution. Static imports would be\r\n // hoisted to module-instantiation time (before the early-phase await\r\n // resolves), which is exactly the bug this split is meant to fix.\r\n // Requires `splitting: true` in esbuild (set below in bundle()).\r\n imports.push(\"\", \"// 4. Load app code (events, locales, main, routes)\");\r\n\r\n if (this.generatedFiles.events) {\r\n imports.push('await import(\"./events\");');\r\n }\r\n if (this.generatedFiles.locales) {\r\n imports.push('await import(\"./locales\");');\r\n }\r\n if (this.generatedFiles.main) {\r\n imports.push('await import(\"./main\");');\r\n }\r\n if (this.generatedFiles.routes) {\r\n imports.push('await import(\"./routes\");');\r\n }\r\n\r\n // Start late-phase connectors after app code registers routes/listeners\r\n imports.push(\r\n \"\",\r\n \"// 5. Start late-phase connectors (http, socket) — routes and\",\r\n \"// listeners registered by app code are now ready to bind\",\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\",\r\n \"connectorsManager.shutdownOnProcessKill();\",\r\n );\r\n\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, \"app.ts\"), content);\r\n }\r\n\r\n /**\r\n * Bundle with esbuild\r\n */\r\n private async bundle(): Promise<void> {\r\n console.log(colors.magenta(\" Bundling with esbuild...\"));\r\n\r\n const entryPoint = path.join(this.productionDir, \"app.ts\");\r\n const outDir = this.options!.outDirectory!;\r\n const outFileName = this.options!.outFile!;\r\n // Strip extension so entryNames produces \"<base>.js\" via esbuild\r\n const entryName = path.basename(outFileName, path.extname(outFileName));\r\n\r\n await ensureDirectoryAsync(outDir);\r\n\r\n const alias = this.buildAliasMapFromTsconfig();\r\n\r\n await esbuild.build({\r\n platform: \"node\",\r\n entryPoints: [entryPoint],\r\n bundle: true,\r\n // Required so dynamic `await import(\"./main\")` in the generated\r\n // app.ts produces separate chunks loaded at the runtime call site,\r\n // instead of inlining the modules at instantiation time (which\r\n // would defeat the early/late phase split).\r\n splitting: true,\r\n packages: \"external\",\r\n minify: this.options!.minify,\r\n sourcemap: this.options!.sourcemap === true ? \"linked\" : this.options!.sourcemap,\r\n format: \"esm\",\r\n // Targeting a concrete Node version (not \"esnext\") so esbuild\r\n // transpiles TC39 stage 3 decorators into helpers — Node does not\r\n // implement them natively yet.\r\n target: [\"node22\"],\r\n outdir: outDir,\r\n entryNames: entryName,\r\n alias,\r\n plugins: [nativeNodeModulesPlugin],\r\n });\r\n }\r\n\r\n /**\r\n * Build an alias map from tsconfig `paths` so esbuild resolves local\r\n * source aliases (e.g. `@warlock.js/cascade`) to their on-disk source\r\n * folders during bundling. Without this, `packages: \"external\"` would\r\n * leave those bare specifiers as raw imports and Node would fail to\r\n * resolve them at runtime (they aren't installed in node_modules).\r\n *\r\n * Only exact (non-wildcard) aliases are included — esbuild's `alias`\r\n * option doesn't support glob-style mappings.\r\n */\r\n private buildAliasMapFromTsconfig(): Record<string, string> {\r\n tsconfigManager.init();\r\n\r\n const alias: Record<string, string> = {};\r\n const baseUrl = path.resolve(process.cwd(), tsconfigManager.baseUrl);\r\n\r\n for (const [from, to] of Object.entries(tsconfigManager.aliases)) {\r\n if (from.endsWith(\"/*\") || !Array.isArray(to) || to.length === 0) {\r\n continue;\r\n }\r\n\r\n alias[from] = path.resolve(baseUrl, to[0]);\r\n }\r\n\r\n return alias;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"production-builder.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/production/production-builder.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n putFileAsync,\r\n removeDirectoryAsync,\r\n} from \"@warlock.js/fs\";\r\nimport esbuild from \"esbuild\";\r\nimport glob from \"fast-glob\";\r\nimport path from \"path\";\r\nimport { tsconfigManager } from \"../dev-server/tsconfig-manager\";\r\nimport { appPath, warlockPath } from \"../utils\";\r\nimport { nativeNodeModulesPlugin } from \"./esbuild-plugins\";\r\nimport { resolveBuildConfig, type ResolvedBuildConfig } from \"./resolve-build-config\";\r\nimport { toCamelCase, toKebabCase } from \"@mongez/reinforcements\";\r\n\r\n/**\r\n * Production Builder\r\n * Generates production-ready files and bundles them for deployment\r\n * Build options are loaded from warlock.config.ts\r\n */\r\nexport class ProductionBuilder {\r\n private options!: ResolvedBuildConfig;\r\n private readonly productionDir = warlockPath(\"production\");\r\n\r\n /**\r\n * Main build entry point\r\n */\r\n public async build(): Promise<void> {\r\n console.log(colors.cyan(\"Building for production...\\n\"));\r\n\r\n // Step 1: Initialize options from config\r\n await this.initializeOptions();\r\n\r\n // Step 2: Generate combined files\r\n await this.generateCombinedFiles();\r\n\r\n // Step 3: Generate entry point\r\n await this.generateEntryPoint();\r\n\r\n // Step 4: Bundle with esbuild\r\n await this.bundle();\r\n\r\n // // Step 5: Remove production folder\r\n await removeDirectoryAsync(this.productionDir);\r\n\r\n console.log(colors.green(\"Build complete!\"));\r\n console.log(`Start production server by running ${colors.cyan(\"warlock start\")}`);\r\n }\r\n\r\n /**\r\n * Initialize options from warlock.config.ts\r\n */\r\n private async initializeOptions(): Promise<void> {\r\n this.options = resolveBuildConfig();\r\n\r\n // Ensure production directory exists\r\n await ensureDirectoryAsync(this.productionDir);\r\n }\r\n\r\n /**\r\n * Track which special files were generated\r\n */\r\n private generatedFiles = {\r\n locales: false,\r\n events: false,\r\n main: false,\r\n routes: false,\r\n };\r\n\r\n /**\r\n * Generate all combined files\r\n */\r\n private async generateCombinedFiles(): Promise<void> {\r\n console.log(colors.yellow(\" Generating production files...\"));\r\n\r\n // Generate bootstrap.ts file\r\n await this.generateBootstrap();\r\n\r\n // Generate config loader\r\n await this.generateConfigLoader();\r\n\r\n // Generate special files and track which ones have content\r\n const [locales, events, main, routes] = await Promise.all([\r\n this.generateLocales(),\r\n this.generateEvents(),\r\n this.generateMain(),\r\n this.generateRoutes(),\r\n ]);\r\n\r\n this.generatedFiles = { locales, events, main, routes };\r\n }\r\n\r\n /**\r\n * Generate bootstrap.ts - ensures bootstrap() runs first and sets production environment\r\n */\r\n private async generateBootstrap(): Promise<void> {\r\n let content = `import { bootstrap, Application } from \"@warlock.js/core\";\r\n\r\n// Set production environment\r\nApplication.setRuntimeStrategy(\"production\");\r\nApplication.setEnvironment(\"production\");\r\n\r\n// Bootstrap the application\r\nbootstrap();\r\n`;\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n content += \"import './../../src/app/bootstrap';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"bootstrap.ts\"), content);\r\n }\r\n\r\n /**\r\n * Glob for module files matching a pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModule(fileName: string): Promise<string[]> {\r\n const pattern = `**/${fileName}.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n // Convert to relative paths from .warlock/production/ to src/app/\r\n // e.g., \"users/main\" -> \"../../src/app/users/main\"\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Glob for files in a specific directory pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModuleDirectory(directory: string): Promise<string[]> {\r\n const pattern = `**/${directory}/*.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Generate config-loader.ts\r\n */\r\n private async generateConfigLoader(): Promise<void> {\r\n const configDirectory = path.join(process.cwd(), \"src/config\");\r\n\r\n const files = await glob(\"*.{ts,tsx}\", {\r\n cwd: configDirectory,\r\n absolute: false,\r\n });\r\n\r\n const configNames = files.map((f) => f.replace(/\\.(ts|tsx)$/, \"\"));\r\n\r\n const imports: string[] = [\r\n 'import config from \"@mongez/config\";',\r\n 'import { configSpecialHandlers } from \"@warlock.js/core\";',\r\n ];\r\n const configImports: string[] = [];\r\n const configSetCalls: string[] = [];\r\n const executors: string[] = [];\r\n\r\n for (const configName of configNames) {\r\n const properConfigName = toCamelCase(configName);\r\n const varName = `${properConfigName}Config`;\r\n configImports.push(`import ${varName} from \"../../src/config/${configName}\";`);\r\n configSetCalls.push(`config.set(\"${properConfigName}\", ${varName});`);\r\n executors.push(`await configSpecialHandlers.execute(\"${properConfigName}\", ${varName});`);\r\n }\r\n\r\n let content = [\r\n ...imports,\r\n \"\",\r\n \"// Config imports\",\r\n ...configImports,\r\n \"\",\r\n \"// Register configs\",\r\n ...configSetCalls,\r\n \"\",\r\n \"// Special handlers\",\r\n ...executors,\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n content += \"import './../../src/app/prestart';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"config-loader.ts\"), content);\r\n }\r\n\r\n /**\r\n * Generate locales.ts (only if there are locale files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateLocales(): Promise<boolean> {\r\n const files = await this.globModule(\"utils/locales\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"locales.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate events.ts (only if there are event files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateEvents(): Promise<boolean> {\r\n const files = await this.globModuleDirectory(\"events\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"events.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate main.ts (only if there are main files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateMain(): Promise<boolean> {\r\n const files = await this.globModule(\"main\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"main.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate routes.ts (only if there are route files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateRoutes(): Promise<boolean> {\r\n const files = await this.globModule(\"routes\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"routes.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate a file with imports from all given files\r\n */\r\n private async generateImportsFile(importPaths: string[], outputFile: string): Promise<void> {\r\n const imports = importPaths.map((importPath) => `import \"${importPath}\";`);\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, outputFile), content);\r\n }\r\n\r\n /**\r\n * Generate the main entry point (app.ts)\r\n */\r\n private async generateEntryPoint(): Promise<void> {\r\n console.log(colors.yellow(\" Generating entry point...\"));\r\n\r\n // Build imports based on which files were generated\r\n const imports: string[] = [\r\n \"// 1. Bootstrap (loads .env, initializes framework)\",\r\n 'import \"./bootstrap\";',\r\n \"\",\r\n \"// 2. Load configs\",\r\n 'import \"./config-loader\";',\r\n \"\",\r\n \"// 3. Start early-phase connectors (database, cache, logger, ...)\",\r\n \"// so data sources, cache, etc. are ready before app code runs\",\r\n 'import { Application, connectorsManager, ConnectorLifecyclePhase } from \"@warlock.js/core\";',\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\",\r\n ];\r\n\r\n // App code uses dynamic `await import(...)` so each module's side\r\n // effects fire at THIS point in execution. Static imports would be\r\n // hoisted to module-instantiation time (before the early-phase await\r\n // resolves), which is exactly the bug this split is meant to fix.\r\n // Requires `splitting: true` in esbuild (set below in bundle()).\r\n imports.push(\"\", \"// 4. Load app code (events, locales, main, routes)\");\r\n\r\n if (this.generatedFiles.events) {\r\n imports.push('await import(\"./events\");');\r\n }\r\n if (this.generatedFiles.locales) {\r\n imports.push('await import(\"./locales\");');\r\n }\r\n if (this.generatedFiles.main) {\r\n imports.push('await import(\"./main\");');\r\n }\r\n if (this.generatedFiles.routes) {\r\n imports.push('await import(\"./routes\");');\r\n }\r\n\r\n // Start late-phase connectors after app code registers routes/listeners\r\n imports.push(\r\n \"\",\r\n \"// 5. Start late-phase connectors (http, socket) — routes and\",\r\n \"// listeners registered by app code are now ready to bind\",\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\",\r\n \"\",\r\n \"// 6. Signal a complete boot so `Application.onceBooted(...)` listeners fire\",\r\n \"Application.markBooted({ environment: Application.environment, runtimeStrategy: Application.runtimeStrategy });\",\r\n \"connectorsManager.shutdownOnProcessKill();\",\r\n );\r\n\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, \"app.ts\"), content);\r\n }\r\n\r\n /**\r\n * Bundle with esbuild\r\n */\r\n private async bundle(): Promise<void> {\r\n console.log(colors.magenta(\" Bundling with esbuild...\"));\r\n\r\n const entryPoint = path.join(this.productionDir, \"app.ts\");\r\n const outDir = this.options!.outDirectory!;\r\n const outFileName = this.options!.outFile!;\r\n // Strip extension so entryNames produces \"<base>.js\" via esbuild\r\n const entryName = path.basename(outFileName, path.extname(outFileName));\r\n\r\n await ensureDirectoryAsync(outDir);\r\n\r\n const alias = this.buildAliasMapFromTsconfig();\r\n\r\n await esbuild.build({\r\n platform: \"node\",\r\n entryPoints: [entryPoint],\r\n bundle: true,\r\n // Required so dynamic `await import(\"./main\")` in the generated\r\n // app.ts produces separate chunks loaded at the runtime call site,\r\n // instead of inlining the modules at instantiation time (which\r\n // would defeat the early/late phase split).\r\n splitting: true,\r\n packages: \"external\",\r\n minify: this.options!.minify,\r\n sourcemap: this.options!.sourcemap === true ? \"linked\" : this.options!.sourcemap,\r\n format: \"esm\",\r\n // Targeting a concrete Node version (not \"esnext\") so esbuild\r\n // transpiles TC39 stage 3 decorators into helpers — Node does not\r\n // implement them natively yet.\r\n target: [\"node22\"],\r\n outdir: outDir,\r\n entryNames: entryName,\r\n alias,\r\n plugins: [nativeNodeModulesPlugin],\r\n });\r\n }\r\n\r\n /**\r\n * Build an alias map from tsconfig `paths` so esbuild resolves local\r\n * source aliases (e.g. `@warlock.js/cascade`) to their on-disk source\r\n * folders during bundling. Without this, `packages: \"external\"` would\r\n * leave those bare specifiers as raw imports and Node would fail to\r\n * resolve them at runtime (they aren't installed in node_modules).\r\n *\r\n * Only exact (non-wildcard) aliases are included — esbuild's `alias`\r\n * option doesn't support glob-style mappings.\r\n */\r\n private buildAliasMapFromTsconfig(): Record<string, string> {\r\n tsconfigManager.init();\r\n\r\n const alias: Record<string, string> = {};\r\n const baseUrl = path.resolve(process.cwd(), tsconfigManager.baseUrl);\r\n\r\n for (const [from, to] of Object.entries(tsconfigManager.aliases)) {\r\n if (from.endsWith(\"/*\") || !Array.isArray(to) || to.length === 0) {\r\n continue;\r\n }\r\n\r\n alias[from] = path.resolve(baseUrl, to[0]);\r\n }\r\n\r\n return alias;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
//#region ../@warlock.js/core/src/utils/environment.d.ts
|
|
2
2
|
type Environment = "development" | "production" | "test";
|
|
3
|
+
/**
|
|
4
|
+
* How the framework hosts your code — the dev server (watcher + HMR) or the
|
|
5
|
+
* bundled production output. A separate axis from {@link Environment}.
|
|
6
|
+
*/
|
|
7
|
+
type RuntimeStrategy = "production" | "development";
|
|
3
8
|
declare function environment(): Environment;
|
|
4
9
|
declare function setEnvironment(env: Environment): void;
|
|
5
10
|
//#endregion
|
|
6
|
-
export { Environment, environment, setEnvironment };
|
|
11
|
+
export { Environment, RuntimeStrategy, environment, setEnvironment };
|
|
7
12
|
//# sourceMappingURL=environment.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"environment.d.mts","names":[],"sources":["../../../../../../../@warlock.js/core/src/utils/environment.ts"],"mappings":";KAAY,WAAA;AAAA,iBAEI,WAAA,IAAe,WAAW;AAAA,iBAI1B,cAAA,CAAe,GAAgB,EAAX,WAAW"}
|
|
1
|
+
{"version":3,"file":"environment.d.mts","names":[],"sources":["../../../../../../../@warlock.js/core/src/utils/environment.ts"],"mappings":";KAAY,WAAA;AAAZ;;;;AAAA,KAMY,eAAA;AAAA,iBAEI,WAAA,IAAe,WAAW;AAAA,iBAI1B,cAAA,CAAe,GAAgB,EAAX,WAAW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"environment.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/utils/environment.ts"],"sourcesContent":["export type Environment = \"development\" | \"production\" | \"test\";\n\nexport function environment(): Environment {\n return (process.env.NODE_ENV as Environment) || \"development\";\n}\n\nexport function setEnvironment(env: Environment) {\n process.env.NODE_ENV = env;\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"environment.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/utils/environment.ts"],"sourcesContent":["export type Environment = \"development\" | \"production\" | \"test\";\n\n/**\n * How the framework hosts your code — the dev server (watcher + HMR) or the\n * bundled production output. A separate axis from {@link Environment}.\n */\nexport type RuntimeStrategy = \"production\" | \"development\";\n\nexport function environment(): Environment {\n return (process.env.NODE_ENV as Environment) || \"development\";\n}\n\nexport function setEnvironment(env: Environment) {\n process.env.NODE_ENV = env;\n}\n"],"mappings":";AAQA,SAAgB,cAA2B;CACzC,OAAQ,QAAQ,IAAI,YAA4B;AAClD;AAEA,SAAgB,eAAe,KAAkB;CAC/C,QAAQ,IAAI,WAAW;AACzB"}
|
package/esm/utils/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocalizedObject, getLocalized } from "./get-localized.mjs";
|
|
2
|
-
import { Environment, environment, setEnvironment } from "./environment.mjs";
|
|
2
|
+
import { Environment, RuntimeStrategy, environment, setEnvironment } from "./environment.mjs";
|
|
3
3
|
import { appLog } from "./app-log.mjs";
|
|
4
4
|
import { DatabaseLog, DatabaseLogOptions } from "./database-log.mjs";
|
|
5
5
|
import { fetchLatestVersion } from "./npm-registry.mjs";
|
package/esm/utils/paths.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import baseConfig from "@mongez/config";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
//#region ../@warlock.js/core/src/utils/paths.ts
|
|
@@ -28,7 +28,7 @@ function storagePath(relativePath = "") {
|
|
|
28
28
|
* If no path is given, it will return the absolute path to the uploads folder
|
|
29
29
|
*/
|
|
30
30
|
function uploadsPath(relativePath = "") {
|
|
31
|
-
const configPath =
|
|
31
|
+
const configPath = baseConfig.get("uploads.root");
|
|
32
32
|
if (!configPath) return rootPath("storage", "uploads", relativePath);
|
|
33
33
|
return typeof configPath === "function" ? configPath(relativePath) : path.resolve(configPath, relativePath);
|
|
34
34
|
}
|
package/esm/utils/paths.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,
|
|
1
|
+
{"version":3,"file":"paths.mjs","names":["config"],"sources":["../../../../../../../@warlock.js/core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAaA,WAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v } from "@warlock.js/seal";
|
|
2
|
-
import
|
|
2
|
+
import baseConfig from "@mongez/config";
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
import { merge } from "@mongez/reinforcements";
|
|
5
5
|
|
|
@@ -40,7 +40,7 @@ async function validateAll(validation, request, response) {
|
|
|
40
40
|
if (validation.validate) {
|
|
41
41
|
const result = await validation.validate(request, response);
|
|
42
42
|
if (result) {
|
|
43
|
-
if (!response.statusCode) response.setStatusCode(
|
|
43
|
+
if (!response.statusCode) response.setStatusCode(baseConfig.get("validation.responseStatus", 400));
|
|
44
44
|
log.info("validation", "failed", "Validation failed");
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request, Response } from \"../http\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate(request, response);\n\n // if there is a result, it means it failed\n if (result) {\n // check if there is no response status code, then set it to config value or 400 as default\n if (!response.statusCode) {\n response.setStatusCode(config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n return result;\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS,SAAS,QAAQ;EAG1D,IAAI,QAAQ;GAEV,IAAI,CAAC,SAAS,YACZ,SAAS,
|
|
1
|
+
{"version":3,"file":"validateAll.mjs","names":["config"],"sources":["../../../../../../../@warlock.js/core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request, Response } from \"../http\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate(request, response);\n\n // if there is a result, it means it failed\n if (result) {\n // check if there is no response status code, then set it to config value or 400 as default\n if (!response.statusCode) {\n response.setStatusCode(config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n return result;\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS,SAAS,QAAQ;EAG1D,IAAI,QAAQ;GAEV,IAAI,CAAC,SAAS,YACZ,SAAS,cAAcA,WAAO,IAAI,6BAA6B,GAAG,CAAC;GAGrE,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAEpD,OAAO;EACT;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
|
package/llms-full.txt
CHANGED
|
@@ -2292,6 +2292,108 @@ There is no `warlock add` feature for password hashing — `bcryptjs` is a plain
|
|
|
2292
2292
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — service layering, where the password boundary should sit.
|
|
2293
2293
|
|
|
2294
2294
|
|
|
2295
|
+
## health-checks `@warlock.js/core/health-checks/SKILL.md`
|
|
2296
|
+
|
|
2297
|
+
---
|
|
2298
|
+
name: health-checks
|
|
2299
|
+
description: 'Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.'
|
|
2300
|
+
---
|
|
2301
|
+
|
|
2302
|
+
# Warlock — health checks & graceful shutdown
|
|
2303
|
+
|
|
2304
|
+
Two endpoints and a drain, so a load balancer never routes to an instance that isn't ready and a deploy never kills an in-flight request. All built in; no controller to hand-roll.
|
|
2305
|
+
|
|
2306
|
+
## The two endpoints
|
|
2307
|
+
|
|
2308
|
+
The HTTP connector registers them on the Fastify server during boot (before route scanning), so they exist by the time the server listens:
|
|
2309
|
+
|
|
2310
|
+
| Path | Probe | 200 when | 503 when |
|
|
2311
|
+
| --- | --- | --- | --- |
|
|
2312
|
+
| `/health` | liveness | the process is up | shutdown has begun |
|
|
2313
|
+
| `/ready` | readiness | booted **and** not shutting down **and** every check passes | before boot, during shutdown, or any failing check |
|
|
2314
|
+
|
|
2315
|
+
**Liveness** answers "should the orchestrator RESTART me?" — it ignores dependency checks (a failing DB doesn't mean restart the pod). **Readiness** answers "should the load balancer ROUTE to me?" — it gates on boot completion, shutdown state, and your registered checks.
|
|
2316
|
+
|
|
2317
|
+
```
|
|
2318
|
+
GET /health → 200 {"status":"ok"}
|
|
2319
|
+
GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
2320
|
+
→ 503 {"status":"error","checks":{"db":false}}
|
|
2321
|
+
```
|
|
2322
|
+
|
|
2323
|
+
### Config
|
|
2324
|
+
|
|
2325
|
+
```ts title="src/config/http.ts"
|
|
2326
|
+
const httpConfigurations: HttpConfigurations = {
|
|
2327
|
+
health: {
|
|
2328
|
+
enabled: true, // default; set false to remove both endpoints
|
|
2329
|
+
path: "/health", // liveness path
|
|
2330
|
+
readinessPath: "/ready", // readiness path
|
|
2331
|
+
},
|
|
2332
|
+
};
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
## Readiness checks
|
|
2336
|
+
|
|
2337
|
+
Readiness is `isBooted && !isShuttingDown` plus every registered check. Register a check from a connector, a `main.ts`, or anywhere:
|
|
2338
|
+
|
|
2339
|
+
```ts
|
|
2340
|
+
import { health } from "@warlock.js/core";
|
|
2341
|
+
|
|
2342
|
+
health.addCheck("db", async () => {
|
|
2343
|
+
return database.isConnected();
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
health.removeCheck("db"); // unregister later if needed
|
|
2347
|
+
```
|
|
2348
|
+
|
|
2349
|
+
A check returns `boolean | Promise<boolean>`. **A thrown error counts as a failed check** (it's surfaced in the `checks` map + the 503, not logged — probes poll often, so a failure is a normal signal, not an error event). Keep checks cheap and fast; they run on every `/ready` poll.
|
|
2350
|
+
|
|
2351
|
+
## Graceful shutdown (request draining)
|
|
2352
|
+
|
|
2353
|
+
On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks → connectors in reverse priority**. The HTTP connector's teardown drains instead of dropping:
|
|
2354
|
+
|
|
2355
|
+
1. `Application.isShuttingDown` flips `true` at the very start → `/ready` immediately returns 503, so the load balancer stops sending new traffic.
|
|
2356
|
+
2. Fastify stops accepting new requests (answers 503 while closing) and lets in-flight ones finish.
|
|
2357
|
+
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
2358
|
+
|
|
2359
|
+
```ts title="src/config/http.ts"
|
|
2360
|
+
const httpConfigurations: HttpConfigurations = {
|
|
2361
|
+
gracefulShutdown: {
|
|
2362
|
+
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
2363
|
+
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
2364
|
+
},
|
|
2365
|
+
};
|
|
2366
|
+
```
|
|
2367
|
+
|
|
2368
|
+
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
2369
|
+
|
|
2370
|
+
## The zero-downtime deploy flow
|
|
2371
|
+
|
|
2372
|
+
```
|
|
2373
|
+
SIGTERM → isShuttingDown = true → /ready returns 503
|
|
2374
|
+
→ LB stops routing new requests to this instance
|
|
2375
|
+
→ in-flight requests drain (up to gracefulShutdown.timeout)
|
|
2376
|
+
→ app onShutdown hooks already ran (db/cache still up)
|
|
2377
|
+
→ connectors close in reverse → process exits
|
|
2378
|
+
```
|
|
2379
|
+
|
|
2380
|
+
For an even smoother handoff, give the load balancer time to observe the 503 before the server closes — e.g. an `onShutdown` hook with a short `sleep` matched to your LB's health-check interval.
|
|
2381
|
+
|
|
2382
|
+
## Gotchas
|
|
2383
|
+
|
|
2384
|
+
- **`/health` is registered straight on Fastify, not the app router.** It's infra, so it's immune to HMR and route scanning — but if your app also defines a `/health` route you'll have a collision. Rename via `http.health.path`.
|
|
2385
|
+
- **Readiness needs a finished boot.** Before `Application.isBooted` (e.g. while late-phase connectors are still starting) `/ready` is 503 by design — that's the point.
|
|
2386
|
+
- **A hanging `onShutdown` hook delays the drain.** App hooks run before connector teardown and are only bounded by your process manager's kill timeout; keep them fast. The HTTP drain itself is bounded by `gracefulShutdown.timeout`.
|
|
2387
|
+
- **The `maintenance` middleware is a different 503.** It allowlists `/health` by default so probes pass during maintenance — but maintenance mode is operator-toggled downtime, not readiness. See `@warlock.js/core/use-middleware/SKILL.md`.
|
|
2388
|
+
|
|
2389
|
+
## See also
|
|
2390
|
+
|
|
2391
|
+
- [`use-app-context/SKILL.md`](../use-app-context/SKILL.md) — `Application.onShutdown` / `onceBooted` / `isShuttingDown`, the lifecycle hooks the endpoints build on.
|
|
2392
|
+
- [`configure-app/SKILL.md`](../configure-app/SKILL.md) — the `src/config/http.ts` shape.
|
|
2393
|
+
- [`add-connector/SKILL.md`](../add-connector/SKILL.md) — connector boot/shutdown order, where draining slots in.
|
|
2394
|
+
- [`use-middleware/SKILL.md`](../use-middleware/SKILL.md) — maintenance mode and other built-in middleware.
|
|
2395
|
+
|
|
2396
|
+
|
|
2295
2397
|
## lower-stage3-decorators `@warlock.js/core/lower-stage3-decorators/SKILL.md`
|
|
2296
2398
|
|
|
2297
2399
|
---
|
|
@@ -5179,7 +5281,7 @@ The reverse direction — sending a stored file back via `response.sendFile(...)
|
|
|
5179
5281
|
|
|
5180
5282
|
---
|
|
5181
5283
|
name: use-app-context
|
|
5182
|
-
description: 'Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.'
|
|
5284
|
+
description: 'Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy, boot lifecycle) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `Application.onceBooted`, `Application.whenBooted`, `Application.isBooted`, `Application.onShutdown`, `Application.isShuttingDown`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check", "run code once the app is fully booted", "after all connectors started", "app booted hook", "run cleanup before shutdown", "graceful shutdown hook"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.'
|
|
5183
5285
|
---
|
|
5184
5286
|
|
|
5185
5287
|
# Warlock — use the application context
|
|
@@ -5254,6 +5356,64 @@ The dev-server CLI command sets it to `"development"`. The `build` command and `
|
|
|
5254
5356
|
|
|
5255
5357
|
Most app code shouldn't care about `runtimeStrategy` — branch on `environment` instead, which is the orthogonal "what world is this code talking to?" axis.
|
|
5256
5358
|
|
|
5359
|
+
## Lifecycle: boot & shutdown
|
|
5360
|
+
|
|
5361
|
+
`Application.onceBooted(callback)` runs a callback the moment the app is fully booted — every connector in **both** phases is active and all app files (locales, events, main, routes) are loaded. It's the only hook that fires *after* the late phase (http, socket) is up.
|
|
5362
|
+
|
|
5363
|
+
```ts
|
|
5364
|
+
import { Application } from "@warlock.js/core";
|
|
5365
|
+
|
|
5366
|
+
Application.onceBooted(({ environment, runtimeStrategy, bootDurationMs }) => {
|
|
5367
|
+
// http is listening, socket is bound, every model is registered
|
|
5368
|
+
});
|
|
5369
|
+
```
|
|
5370
|
+
|
|
5371
|
+
Why a dedicated hook instead of code at the bottom of `main.ts`? **App files load before the late phase.** `main.ts`, `events.ts`, `routes.ts`, and locales are all imported *between* the early and late connector phases — so when `main.ts` runs, http/socket are not listening yet. `onceBooted` defers your callback until the whole sequence finishes, which makes it the safe place to touch `app.http` / `app.socket` from `main.ts`-level code.
|
|
5372
|
+
|
|
5373
|
+
It's a **latch**, not a plain event listener: register before boot and it queues; register *after* boot and it fires on the next microtask. A late subscriber never silently misses the signal.
|
|
5374
|
+
|
|
5375
|
+
```ts
|
|
5376
|
+
Application.isBooted; // boolean — has boot finished?
|
|
5377
|
+
await Application.whenBooted(); // Promise<BootContext> — await instead of a callback
|
|
5378
|
+
```
|
|
5379
|
+
|
|
5380
|
+
`whenBooted()` is the promise form, resolving with the same `BootContext` (`{ environment, runtimeStrategy, bootDurationMs? }`). `bootDurationMs` is set by the dev server (which times boot) and omitted by the production entry.
|
|
5381
|
+
|
|
5382
|
+
The framework fires the latch for you — the dev server and the production bundle each call it once, right after the late phase. App code only ever *reads* it via `onceBooted` / `whenBooted` / `isBooted`; the internal `markBooted` is a framework entry point, not for application use.
|
|
5383
|
+
|
|
5384
|
+
### Run something once everything is listening
|
|
5385
|
+
|
|
5386
|
+
```ts title="src/app/main.ts"
|
|
5387
|
+
import { Application } from "@warlock.js/core";
|
|
5388
|
+
import { log } from "@warlock.js/logger";
|
|
5389
|
+
|
|
5390
|
+
Application.onceBooted(({ environment, bootDurationMs }) => {
|
|
5391
|
+
log.success("app", "booted", `ready in ${environment}`, { bootDurationMs });
|
|
5392
|
+
});
|
|
5393
|
+
```
|
|
5394
|
+
|
|
5395
|
+
This is the right home for "warm a cache", "register a recurring job", "ping a readiness endpoint", or "open an outbound connection that needs the http server already listening" — anything that must wait for a complete boot.
|
|
5396
|
+
|
|
5397
|
+
### Shutdown — clean up before the app goes down
|
|
5398
|
+
|
|
5399
|
+
`Application.onShutdown(callback)` is the mirror of `onceBooted`: it runs your teardown **once**, when the process is shutting down (SIGINT/SIGTERM, or the dev server stopping) — and crucially **before** the connectors (db, cache, http) are torn down, so your cleanup can still use them.
|
|
5400
|
+
|
|
5401
|
+
```ts title="src/app/main.ts"
|
|
5402
|
+
import { Application } from "@warlock.js/core";
|
|
5403
|
+
|
|
5404
|
+
Application.onceBooted(() => {
|
|
5405
|
+
const consumer = startQueueConsumer();
|
|
5406
|
+
|
|
5407
|
+
Application.onShutdown(async () => {
|
|
5408
|
+
await consumer.stop(); // db / cache / http are still up here
|
|
5409
|
+
});
|
|
5410
|
+
});
|
|
5411
|
+
```
|
|
5412
|
+
|
|
5413
|
+
Hooks run **LIFO** (reverse of registration — last opened, first closed), each is awaited, and a throwing hook is caught + logged so it can't block the rest. `Application.isShuttingDown` flips `true` the moment shutdown begins — the built-in `/ready` endpoint reads it to report not-ready so a load balancer drains the instance first. Like `onceBooted`, registering after shutdown has begun runs the callback immediately.
|
|
5414
|
+
|
|
5415
|
+
The framework triggers this for you (the connectors manager runs the hooks at the start of shutdown); app code only ever registers via `onShutdown`. For the HTTP-side story — `/health`, `/ready`, and graceful request draining — see `@warlock.js/core/health-checks/SKILL.md`.
|
|
5416
|
+
|
|
5257
5417
|
## Paths
|
|
5258
5418
|
|
|
5259
5419
|
For path helpers (`appPath`, `configPath`, `uploadsPath`, …) anchored at `process.cwd()`, the `paths.*` aggregate, and the `uploads.root` config override, see [`resolve-path/SKILL.md`](../resolve-path/SKILL.md).
|
|
@@ -5409,7 +5569,9 @@ cors: {
|
|
|
5409
5569
|
|
|
5410
5570
|
- **`version` is `null` until the first `await`.** The version loader is async (it reads `package.json`). On a cold start before any framework code has run `getWarlockVersion()`, `Application.version` returns `null`. The framework does load it during bootstrap, so anywhere downstream of bootstrap is fine — controllers, services, connectors after `start()`. CLI commands without `preload.bootstrap` may see `null`.
|
|
5411
5571
|
- **`Application` is static, not a DI registration.** Don't try to inject it. There's nothing to inject — it's a class with only static members.
|
|
5412
|
-
- **`app.*` accessors return `undefined` before their connector boots — they don't throw.** `app.socket` / `app.database` / `app.http` are populated by their respective connectors during boot; until then each getter returns `undefined` (a bare `container.get(...)`). Reading them earlier (eager module-load code, the top level of a `main.ts` for the late-phase `http`/`socket`, certain CLI commands without the right `preload.connectors`) hands you `undefined`, and chaining off it throws. Safe everywhere downstream of bootstrap.
|
|
5572
|
+
- **`app.*` accessors return `undefined` before their connector boots — they don't throw.** `app.socket` / `app.database` / `app.http` are populated by their respective connectors during boot; until then each getter returns `undefined` (a bare `container.get(...)`). Reading them earlier (eager module-load code, the top level of a `main.ts` for the late-phase `http`/`socket`, certain CLI commands without the right `preload.connectors`) hands you `undefined`, and chaining off it throws. Safe everywhere downstream of bootstrap — including inside `Application.onceBooted(...)`.
|
|
5573
|
+
- **`onceBooted` is a latch, not `events.on`.** A callback registered *after* boot completed still runs (next microtask) instead of silently missing the signal — so register it wherever it reads best, including module top-level in `main.ts`. A listener that throws is caught and logged; it can't break boot or the other listeners.
|
|
5574
|
+
- **`onShutdown` runs before connectors close, not after.** That ordering is deliberate so cleanup can still use db/cache/http — but it means a hook that hangs delays connector teardown (bounded only by your process manager's kill timeout). Keep teardown fast; for HTTP draining the framework already bounds it via `http.gracefulShutdown.timeout`.
|
|
5413
5575
|
|
|
5414
5576
|
## See also
|
|
5415
5577
|
|
package/llms.txt
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
|
|
17
17
|
- [encrypt-data](@warlock.js/core/encrypt-data/SKILL.md): Reversible AES-256-GCM `encrypt` / `decrypt` for secrets you need to read back; one-way HMAC-SHA256 `hmacHash` for deterministic fingerprints (lookup/dedup of encrypted columns). Keys come from `src/config/encryption.ts`. Triggers: `encrypt`, `decrypt`, `hmacHash`, `EncryptionConfigurations`, `APP_ENCRYPTION_KEY`, `APP_HMAC_KEY`; "store an API key reversibly", "fingerprint an encrypted column for lookup", "AES-256-GCM secret", "HMAC-SHA256 dedup key"; typical import `import { encrypt, decrypt, hmacHash } from "@warlock.js/core"`. Skip: password hashing — `@warlock.js/core/hash-password/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs Node `crypto` direct, `crypto-js`, `libsodium-wrappers`.
|
|
18
18
|
- [hash-password](@warlock.js/core/hash-password/SKILL.md): One-way bcrypt password hashing — `hashPassword` / `verifyPassword`, plus the declarative `useHashedPassword()` schema transformer that auto-hashes a model's password field on save. Salt rounds come from `src/config/encryption.ts`. Triggers: `hashPassword`, `verifyPassword`, `useHashedPassword`, `password.salt`, `bcryptjs`; "hash a user password", "verify login credentials", "auto-hash on save", "rotate a password"; typical import `import { hashPassword, verifyPassword } from "@warlock.js/core"`. Skip: reversible secrets — `@warlock.js/core/encrypt-data/SKILL.md`; the other transformers — `@warlock.js/core/use-model-transformers/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `bcrypt` native, `argon2`, `scrypt`.
|
|
19
|
+
- [health-checks](@warlock.js/core/health-checks/SKILL.md): Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.
|
|
19
20
|
- [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
|
|
20
21
|
- [process-image](@warlock.js/core/process-image/SKILL.md): Transform images with the `Image` class — resize, crop, rotate, format, quality, watermark, blur, etc. — using a deferred pipeline that runs only at `save()` / `toBuffer()` / `toBase64()` / `toDataUrl()` time. Requires sharp via `warlock add image`. Triggers: `Image`, `Image.fromFile`, `Image.fromBuffer`, `Image.fromUrl`, `.resize`, `.crop`, `.watermark`, `.toBuffer`, `.toDataUrl`, `.apply`; "resize an image", "generate a thumbnail", "watermark a product photo", "build an image pipeline"; typical import `import { Image } from "@warlock.js/core"`. Skip: multipart upload entry — `@warlock.js/core/upload-file/SKILL.md`; storage persistence — `@warlock.js/core/store-file/SKILL.md`; competing libs `sharp` direct, `jimp`, `imagemagick`, `gm`.
|
|
21
22
|
- [register-route](@warlock.js/core/register-route/SKILL.md): Register HTTP routes via @warlock.js/core's router — single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains. Routes always live in `src/app/<module>/routes.ts`. Triggers: `router.get`, `router.post`, `router.prefix`, `router.group`, `router.route`, `guarded`; "add a route", "wire a controller to a URL", "group routes by prefix", "register a RESTful resource"; typical import `import { router } from "@warlock.js/core"`. Skip: handler shape — `@warlock.js/core/create-controller/SKILL.md`; CRUD chain details — `@warlock.js/core/build-restful/SKILL.md`; middleware authoring — `@warlock.js/core/write-middleware/SKILL.md`; competing libs `express`, `fastify`, `koa`, `@nestjs/common`.
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
- [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
|
|
30
31
|
- [update-packages](@warlock.js/core/update-packages/SKILL.md): Keep a project current with `warlock update` — bump every `@warlock.js/*` dependency in package.json to its latest published version (range operator preserved), then run the lockfile-detected package manager install. Also covers the `warlock dev` update notice and the `devServer.checkForUpdates` toggle. Triggers: `warlock update`, `--no-install`, `checkForUpdates`, `fetchLatestVersion`, `isNewerVersion`; "update warlock packages", "upgrade the framework", "is there a new warlock version", "update notice in the dev server", "bump @warlock.js/* to latest"; typical CLI `warlock update`. Skip: dev/build/start runtime — `@warlock.js/core/run-app/SKILL.md`; writing a custom command — `@warlock.js/core/write-cli-command/SKILL.md`; installing a NEW feature package (auth, mail, storage) — that is `warlock add`; releasing/publishing the framework — workspace release tooling, not this command.
|
|
31
32
|
- [upload-file](@warlock.js/core/upload-file/SKILL.md): Handle multipart file uploads — read via `request.file()` or `request.validated()`, validate with `v.file()`, save via `UploadedFile.save()` or the storage layer, transform images inline. Triggers: `UploadedFile`, `request.file`, `v.file`, `.save`, `.saveAs`, `.resize`, `.format`, `.quality`, `.image`, `.mimeType`, `.maxSize`; "accept a file upload", "validate file size and mime", "save to S3 or local disk", "resize an uploaded image on save"; typical import `import type { UploadedFile, RequestHandler } from "@warlock.js/core"`. Skip: storage drivers + presigned URLs — `@warlock.js/core/store-file/SKILL.md`; image-only transforms — `@warlock.js/core/process-image/SKILL.md`; schema rules — `@warlock.js/core/validate-input/SKILL.md`; competing libs `multer`, `formidable`, `busboy`.
|
|
32
|
-
- [use-app-context](@warlock.js/core/use-app-context/SKILL.md): Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.
|
|
33
|
+
- [use-app-context](@warlock.js/core/use-app-context/SKILL.md): Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy, boot lifecycle) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `Application.onceBooted`, `Application.whenBooted`, `Application.isBooted`, `Application.onShutdown`, `Application.isShuttingDown`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check", "run code once the app is fully booted", "after all connectors started", "app booted hook", "run cleanup before shutdown", "graceful shutdown hook"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.
|
|
33
34
|
- [use-localization](@warlock.js/core/use-localization/SKILL.md): Multi-locale translations via `groupedTranslations` (declare keys), `t()` / `request.t()` / `request.trans()` (look up), `request.getLocaleCode()` (detect locale from headers/query), `getLocalized` (pick the right value from a localized-array column). Triggers: `groupedTranslations`, `t`, `request.t`, `request.trans`, `request.transFrom`, `request.getLocaleCode`, `request.setLocaleCode`, `getLocalized`; "add a translation key", "resolve a localized error message", "detect request locale", "pick the right per-locale column value"; typical import `import { t, getLocalized } from "@warlock.js/core"`. Skip: resource output — `@warlock.js/core/define-resource/SKILL.md`; module scaffold — `@warlock.js/core/create-module/SKILL.md`; competing libs `i18next`, `react-intl`, raw `@mongez/localization`.
|
|
34
35
|
- [use-middleware](@warlock.js/core/use-middleware/SKILL.md): Attach built-in HTTP middleware to routes via the `middleware` namespace from `@warlock.js/core` — rateLimit, concurrencyLimit, maxBodySize, idempotency, maintenance, ipFilter, cache. Plus `X-Request-Id` correlation, wired automatically. Triggers: `middleware.rateLimit`, `middleware.concurrencyLimit`, `middleware.maxBodySize`, `middleware.idempotency`, `middleware.maintenance`, `middleware.ipFilter`, `middleware.cache`, `X-Request-Id`, `Idempotency-Key`; "add rate limiting", "dedupe writes by idempotency key", "cap concurrent requests", "block IPs", "cache a GET response"; typical import `import { middleware } from "@warlock.js/core"`. Skip: author custom middleware — `@warlock.js/core/write-middleware/SKILL.md`; cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `@fastify/rate-limit` direct, `express-rate-limit`, `helmet`.
|
|
35
36
|
- [use-model-transformers](@warlock.js/core/use-model-transformers/SKILL.md): Three schema-side helpers — `useHashedPassword()` (bcrypt on save) attaches via `.addTransformer(...)`; `useComputedSlug(field?, scope?)` (auto-slug from another field) and `useComputedModel(callback)` (arbitrary computed-on-save value) attach via `v.computed(...)`. Triggers: `useHashedPassword`, `useComputedSlug`, `useComputedModel`, `.addTransformer`, `v.computed`, `ComputedCallback`; "auto-hash a password field", "auto-slug from title on save", "derive a value at write time", "declarative model transformers"; typical import `import { useHashedPassword, useComputedSlug } from "@warlock.js/core"`. Skip: bcrypt setup details — `@warlock.js/core/hash-password/SKILL.md`; repository writes — `@warlock.js/core/use-repository/SKILL.md`; output filtering — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: manual `await hashPassword(input)` in services, ORM lifecycle hooks.
|
package/package.json
CHANGED
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"@mongez/slug": "^1.0.7",
|
|
37
37
|
"@mongez/supportive-is": "^2.1.3",
|
|
38
38
|
"@mongez/time-wizard": "^1.0.6",
|
|
39
|
-
"@warlock.js/auth": "4.
|
|
40
|
-
"@warlock.js/cache": "4.
|
|
41
|
-
"@warlock.js/cascade": "4.
|
|
42
|
-
"@warlock.js/context": "4.
|
|
43
|
-
"@warlock.js/logger": "4.
|
|
44
|
-
"@warlock.js/seal": "4.
|
|
45
|
-
"@warlock.js/fs": "4.
|
|
39
|
+
"@warlock.js/auth": "4.4.0",
|
|
40
|
+
"@warlock.js/cache": "4.4.0",
|
|
41
|
+
"@warlock.js/cascade": "4.4.0",
|
|
42
|
+
"@warlock.js/context": "4.4.0",
|
|
43
|
+
"@warlock.js/logger": "4.4.0",
|
|
44
|
+
"@warlock.js/seal": "4.4.0",
|
|
45
|
+
"@warlock.js/fs": "4.4.0",
|
|
46
46
|
"chokidar": "^5.0.0",
|
|
47
47
|
"dayjs": "^1.11.19",
|
|
48
48
|
"es-module-lexer": "^2.0.0",
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
"react": "^19.2.3",
|
|
69
69
|
"react-dom": "^19.2.3",
|
|
70
70
|
"@react-email/render": "^2.0.5",
|
|
71
|
-
"@warlock.js/herald": "4.
|
|
71
|
+
"@warlock.js/herald": "4.4.0"
|
|
72
72
|
},
|
|
73
73
|
"bin": {
|
|
74
74
|
"warlock": "bin/warlock.js"
|
|
75
75
|
},
|
|
76
|
-
"version": "4.
|
|
76
|
+
"version": "4.4.0",
|
|
77
77
|
"type": "module",
|
|
78
78
|
"main": "./esm/index.mjs",
|
|
79
79
|
"module": "./esm/index.mjs",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: health-checks
|
|
3
|
+
description: 'Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — health checks & graceful shutdown
|
|
7
|
+
|
|
8
|
+
Two endpoints and a drain, so a load balancer never routes to an instance that isn't ready and a deploy never kills an in-flight request. All built in; no controller to hand-roll.
|
|
9
|
+
|
|
10
|
+
## The two endpoints
|
|
11
|
+
|
|
12
|
+
The HTTP connector registers them on the Fastify server during boot (before route scanning), so they exist by the time the server listens:
|
|
13
|
+
|
|
14
|
+
| Path | Probe | 200 when | 503 when |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `/health` | liveness | the process is up | shutdown has begun |
|
|
17
|
+
| `/ready` | readiness | booted **and** not shutting down **and** every check passes | before boot, during shutdown, or any failing check |
|
|
18
|
+
|
|
19
|
+
**Liveness** answers "should the orchestrator RESTART me?" — it ignores dependency checks (a failing DB doesn't mean restart the pod). **Readiness** answers "should the load balancer ROUTE to me?" — it gates on boot completion, shutdown state, and your registered checks.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
GET /health → 200 {"status":"ok"}
|
|
23
|
+
GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
24
|
+
→ 503 {"status":"error","checks":{"db":false}}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Config
|
|
28
|
+
|
|
29
|
+
```ts title="src/config/http.ts"
|
|
30
|
+
const httpConfigurations: HttpConfigurations = {
|
|
31
|
+
health: {
|
|
32
|
+
enabled: true, // default; set false to remove both endpoints
|
|
33
|
+
path: "/health", // liveness path
|
|
34
|
+
readinessPath: "/ready", // readiness path
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Readiness checks
|
|
40
|
+
|
|
41
|
+
Readiness is `isBooted && !isShuttingDown` plus every registered check. Register a check from a connector, a `main.ts`, or anywhere:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { health } from "@warlock.js/core";
|
|
45
|
+
|
|
46
|
+
health.addCheck("db", async () => {
|
|
47
|
+
return database.isConnected();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
health.removeCheck("db"); // unregister later if needed
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A check returns `boolean | Promise<boolean>`. **A thrown error counts as a failed check** (it's surfaced in the `checks` map + the 503, not logged — probes poll often, so a failure is a normal signal, not an error event). Keep checks cheap and fast; they run on every `/ready` poll.
|
|
54
|
+
|
|
55
|
+
## Graceful shutdown (request draining)
|
|
56
|
+
|
|
57
|
+
On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks → connectors in reverse priority**. The HTTP connector's teardown drains instead of dropping:
|
|
58
|
+
|
|
59
|
+
1. `Application.isShuttingDown` flips `true` at the very start → `/ready` immediately returns 503, so the load balancer stops sending new traffic.
|
|
60
|
+
2. Fastify stops accepting new requests (answers 503 while closing) and lets in-flight ones finish.
|
|
61
|
+
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
62
|
+
|
|
63
|
+
```ts title="src/config/http.ts"
|
|
64
|
+
const httpConfigurations: HttpConfigurations = {
|
|
65
|
+
gracefulShutdown: {
|
|
66
|
+
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
67
|
+
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
73
|
+
|
|
74
|
+
## The zero-downtime deploy flow
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
SIGTERM → isShuttingDown = true → /ready returns 503
|
|
78
|
+
→ LB stops routing new requests to this instance
|
|
79
|
+
→ in-flight requests drain (up to gracefulShutdown.timeout)
|
|
80
|
+
→ app onShutdown hooks already ran (db/cache still up)
|
|
81
|
+
→ connectors close in reverse → process exits
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For an even smoother handoff, give the load balancer time to observe the 503 before the server closes — e.g. an `onShutdown` hook with a short `sleep` matched to your LB's health-check interval.
|
|
85
|
+
|
|
86
|
+
## Gotchas
|
|
87
|
+
|
|
88
|
+
- **`/health` is registered straight on Fastify, not the app router.** It's infra, so it's immune to HMR and route scanning — but if your app also defines a `/health` route you'll have a collision. Rename via `http.health.path`.
|
|
89
|
+
- **Readiness needs a finished boot.** Before `Application.isBooted` (e.g. while late-phase connectors are still starting) `/ready` is 503 by design — that's the point.
|
|
90
|
+
- **A hanging `onShutdown` hook delays the drain.** App hooks run before connector teardown and are only bounded by your process manager's kill timeout; keep them fast. The HTTP drain itself is bounded by `gracefulShutdown.timeout`.
|
|
91
|
+
- **The `maintenance` middleware is a different 503.** It allowlists `/health` by default so probes pass during maintenance — but maintenance mode is operator-toggled downtime, not readiness. See `@warlock.js/core/use-middleware/SKILL.md`.
|
|
92
|
+
|
|
93
|
+
## See also
|
|
94
|
+
|
|
95
|
+
- [`use-app-context/SKILL.md`](../use-app-context/SKILL.md) — `Application.onShutdown` / `onceBooted` / `isShuttingDown`, the lifecycle hooks the endpoints build on.
|
|
96
|
+
- [`configure-app/SKILL.md`](../configure-app/SKILL.md) — the `src/config/http.ts` shape.
|
|
97
|
+
- [`add-connector/SKILL.md`](../add-connector/SKILL.md) — connector boot/shutdown order, where draining slots in.
|
|
98
|
+
- [`use-middleware/SKILL.md`](../use-middleware/SKILL.md) — maintenance mode and other built-in middleware.
|