@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
254 lines • 8.81 kB
JavaScript
/**
* @module runtime/module-loader
* @description Module loader with dependency injection and lifecycle management
*/
import { ModuleError, ModuleInitializationError, CircularDependencyError, } from './types/module.js';
import { createModuleRegistry } from './module-registry.js';
import { logger } from './logger.js';
/**
* Module loader with dependency injection
*/
export class ModuleLoader {
registry;
config;
initializingModules = new Set();
constructor(config = {}) {
this.registry = createModuleRegistry();
this.config = {
autoInit: config.autoInit ?? false,
initTimeout: config.initTimeout ?? 30000,
allowCircularDependencies: config.allowCircularDependencies ?? false,
cache: config.cache ?? true,
};
}
/**
* Register a module
*
* @param module - Module to register
* @param gctx - Global context (required if autoInit is true)
* @throws {ModuleError} If module already registered
*/
async register(module, gctx) {
this.registry.register(module);
if (this.config.autoInit && gctx) {
await this.initialize(module.name, gctx);
}
}
/**
* Initialize a module and its dependencies
*
* @param name - Module name to initialize
* @param gctx - Global context
* @param dependencyChain - Internal tracking for circular dependency detection
* @throws {ModuleNotFoundError} If module not found
* @throws {CircularDependencyError} If circular dependency detected
* @throws {ModuleInitializationError} If initialization fails
*/
async initialize(name, gctx, dependencyChain = []) {
const metadata = this.registry.get(name);
// Already initialized
if (metadata.state === 'initialized') {
return;
}
// Check for circular dependencies
if (this.initializingModules.has(name)) {
const chain = [...dependencyChain, name];
if (!this.config.allowCircularDependencies) {
throw new CircularDependencyError(chain);
}
// Allow circular dependencies but don't re-initialize
return;
}
// Mark as initializing
this.initializingModules.add(name);
this.registry.updateState(name, 'initializing');
try {
// Initialize dependencies first
if (metadata.module.dependencies) {
for (const dep of metadata.module.dependencies) {
if (!this.registry.has(dep)) {
throw new ModuleError(`Dependency not found: ${dep} (required by ${name})`, name);
}
await this.initialize(dep, gctx, [...dependencyChain, name]);
}
}
// Initialize module
if (metadata.module.init) {
const initPromise = Promise.resolve(metadata.module.init(gctx));
// Apply timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Module initialization timeout: ${name}`));
}, this.config.initTimeout);
});
await Promise.race([initPromise, timeoutPromise]);
}
// Mark as initialized
this.registry.updateState(name, 'initialized');
this.initializingModules.delete(name);
}
catch (error) {
this.initializingModules.delete(name);
const moduleError = error instanceof ModuleError
? error
: new ModuleInitializationError(name, error);
this.registry.updateState(name, 'error', moduleError);
throw moduleError;
}
}
/**
* Get module exports
*
* @param name - Module name
* @param gctx - Global context (for lazy initialization)
* @returns Module exports
* @throws {ModuleNotFoundError} If module not found
* @throws {ModuleError} If module not initialized and can't auto-initialize
*/
async get(name, gctx) {
const metadata = this.registry.get(name);
// Initialize if needed
if (metadata.state === 'uninitialized') {
if (!gctx) {
throw new ModuleError(`Module not initialized and no global context provided: ${name}`, name);
}
await this.initialize(name, gctx);
}
// Check if in error state
if (metadata.state === 'error') {
throw new ModuleError(`Module is in error state: ${name}`, name, metadata.error);
}
// Check if shutting down
if (metadata.state === 'shutdown') {
throw new ModuleError(`Module has been shutdown: ${name}`, name);
}
// Increment usage count
this.registry.incrementUsage(name);
return metadata.module.exports;
}
/**
* Get module exports synchronously (only works for initialized modules)
*
* @param name - Module name
* @returns Module exports
* @throws {ModuleNotFoundError} If module not found
* @throws {ModuleError} If module not initialized
*/
getSync(name) {
const metadata = this.registry.get(name);
if (metadata.state !== 'initialized') {
throw new ModuleError(`Module not initialized: ${name} (state: ${metadata.state})`, name);
}
this.registry.incrementUsage(name);
return metadata.module.exports;
}
/**
* Shutdown a module
*
* @param name - Module name
* @throws {ModuleNotFoundError} If module not found
*/
async shutdown(name) {
const metadata = this.registry.get(name);
if (metadata.state === 'shutdown') {
return;
}
try {
if (metadata.module.shutdown) {
await Promise.resolve(metadata.module.shutdown());
}
this.registry.updateState(name, 'shutdown');
}
catch (error) {
throw new ModuleError(`Failed to shutdown module: ${name}`, name, error);
}
}
/**
* Shutdown all modules
*/
async shutdownAll() {
// Shutdown in reverse order of initialization
const initialized = this.registry
.getModulesByState('initialized')
.sort((a, b) => {
const aTime = a.initializedAt?.getTime() ?? 0;
const bTime = b.initializedAt?.getTime() ?? 0;
return bTime - aTime; // Reverse order
})
.map((m) => m.module.name);
for (const name of initialized) {
try {
await this.shutdown(name);
}
catch (error) {
// Continue shutting down other modules even if one fails
logger.error({ module: name, error }, 'Failed to shutdown module');
}
}
}
/**
* Check if a module is registered
*
* @param name - Module name
* @returns true if module is registered
*/
has(name) {
return this.registry.has(name);
}
/**
* Get all registered module names
*
* @returns Array of module names
*/
getModuleNames() {
return this.registry.getModuleNames();
}
/**
* Get module statistics
*
* @returns Statistics object
*/
getStatistics() {
return this.registry.getStatistics();
}
/**
* Run health checks on all modules
*
* @returns Map of module name to health status
*/
async healthCheck() {
const results = new Map();
const moduleNames = this.registry.getModuleNames();
for (const name of moduleNames) {
const metadata = this.registry.tryGet(name);
if (!metadata || metadata.state !== 'initialized') {
results.set(name, false);
continue;
}
if (metadata.module.healthCheck) {
try {
const healthy = await Promise.resolve(metadata.module.healthCheck());
results.set(name, healthy);
}
catch {
results.set(name, false);
}
}
else {
// No health check defined, assume healthy if initialized
results.set(name, true);
}
}
return results;
}
}
/**
* Create a new module loader
*
* @param config - Module loader configuration
* @returns New module loader instance
*/
export function createModuleLoader(config) {
return new ModuleLoader(config);
}
//# sourceMappingURL=module-loader.js.map