UNPKG

@stone-js/core

Version:

Stone.js core library, providing the kernel, factory, and context for building applications.

1,480 lines (1,465 loc) 120 kB
import deepmerge from 'deepmerge'; import { isObjectLike, isPlainObject, get, set, isEmpty as isEmpty$1 } from 'lodash-es'; import { isFunction, isConstructor, Pipeline, isClassPipe, isAliasPipe, isFactoryPipe } from '@stone-js/pipeline'; export { isConstructor, isFunction, isString } from '@stone-js/pipeline'; import { Config } from '@stone-js/config'; import { Container } from '@stone-js/service-container'; /** * Class representing a RuntimeError. * * @author Mr. Stone <evensstone@gmail.com> */ class RuntimeError extends Error { code; cause; metadata; /** * Create a RuntimeError. * * @param options - The options to create a RuntimeError. * @returns A new RuntimeError instance. */ static create(message, options = {}) { return new this(message, options); } /** * Create a RuntimeError. * * @param message - The message to log. * @param options - The error options. */ constructor(message, options = {}) { super(message); this.code = options.code; this.name = 'RuntimeError'; this.metadata = options.metadata; if (options.cause !== undefined) { this.cause = options.cause; } if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); // Official V8 method to capture the stack trace, excluding the constructor } else { this.stack = new Error(message).stack; // Fallback for environments without captureStackTrace } } /** * Converts the error to a formatted string representation. * * @param multiline - Determine if output value must be multiline or not. * @returns A formatted error string. */ toString(multiline = false) { const baseMessage = `Error: ${this.name}`; const codeMessage = this.code !== undefined ? `Code: ${String(this.code)}` : ''; const mainMessage = `Message: "${this.message}"`; const metadataMessage = this.metadata !== undefined ? `Metadata: ${JSON.stringify(this.metadata)}` : ''; if (multiline) { return [baseMessage, codeMessage, mainMessage, metadataMessage] .filter(Boolean) .join('\n'); } return [ `[${this.name}]`, this.code !== undefined ? `Code: ${String(this.code)}` : '', `Message: "${this.message}"`, this.metadata !== undefined ? `Metadata: ${JSON.stringify(this.metadata)}` : '' ] .filter(Boolean) .join(', '); } } /** * Custom error for Setup layer operations. */ class SetupError extends RuntimeError { constructor(message, options = {}) { super(message, options); this.name = 'SetupError'; } } /** * Merges multiple blueprints into a single application blueprint. * * This function takes any number of blueprint objects and merges them into one, * with later blueprints overwriting properties of earlier ones in case of conflicts. * It uses deep merging to ensure nested properties are also combined appropriately. * Note: The `deepmerge` function can lead to unexpected results if objects have circular references. * Consider handling such cases or documenting this behavior if it applies to your usage. * * @param blueprints - An array of blueprints to be merged. * @returns The merged application blueprint. * * @throws {SetupError} - If any of the provided blueprints are not valid objects. * * @example * ```typescript * const mergedBlueprint = mergeBlueprints(blueprint1, blueprint2); * ``` */ const mergeBlueprints = (...blueprints) => { validateBlueprints(blueprints); return blueprints.reduce((prev, curr) => deepmerge(prev, curr, { isMergeableObject: isMergeable }), { stone: {} }); }; /** * Check if the provided value is a Stone blueprint. * This function checks if the value is an object and contains the required `stone` property. * * @param value - The value to check. * @returns `true` if the value is a Stone blueprint, otherwise `false`. */ const isStoneBlueprint = (value) => { return typeof value?.stone === 'object'; }; /** * Check if the provided value is an object module. * * @param value - The value to check. * @returns `true` if the value is an object module, otherwise `false`. */ const isObjectLikeModule = (value) => { return isObjectLike(value); }; /** * Check if the provided value is a function module. * * @param value - The value to check. * @returns `true` if the value is a function module, otherwise `false`. */ const isFunctionModule = (value) => { return isFunction(value); }; /** * Check if the provided value is a meta module. * * @param value - The value to check. * @returns `true` if the value is a meta module, otherwise `false`. */ const isMetaModule = (value) => { return isFunction(value?.module); }; /** * Check if the provided value is a meta function module. * * @param value - The value to check. * @returns `true` if the value is a meta function module, otherwise `false`. */ const isMetaFunctionModule = (value) => { return isFunction(value?.module) && value?.isClass !== true && value?.isFactory !== true; }; /** * Check if the provided value is a meta class module. * * @param value - The value to check. * @returns `true` if the value is a meta class module, otherwise `false`. */ const isMetaClassModule = (value) => { return value?.isClass === true && isConstructor(value?.module); }; /** * Check if the provided value is a meta factory module. * * @param value - The value to check. * @returns `true` if the value is a meta factory module, otherwise `false`. */ const isMetaFactoryModule = (value) => { return value?.isFactory === true && isFunction(value?.module); }; /** * Check if the provided value is a meta alias module. * * @param value - The value to check. * @returns `true` if the value is a meta alias module, otherwise `false`. */ const isMetaAliasModule = (value) => { return value?.isAlias === true && isFunction(value?.module); }; /** * Check if the provided handler has the specified hook. * * @param handler - The handler to check. * @param hookName - The hook name to check. * @returns `true` if the handler has the specified hook, otherwise `false`. */ const isHandlerHasHook = (handler, hookName) => { return isFunctionModule(handler?.[hookName]); }; /** * Check if the provided value is not empty. * * @param value - The value to check. * @returns `true` if the value is not empty, otherwise `false`. */ const isNotEmpty = (value) => { if (value === null || value === undefined || value === false || value === 0) return false; if (typeof value === 'string' || Array.isArray(value)) { return value.length > 0; } if (value instanceof Map || value instanceof Set) { return value.size > 0; } if (isPlainObject(value)) { return Object.keys(value).length > 0 || Object.getOwnPropertySymbols(value).length > 0; } return true; }; /** * Check if the provided value is empty. * * @param value - The value to check. * @returns `true` if the value is empty, otherwise `false`. */ const isEmpty = (value) => { return !isNotEmpty(value); }; /** * Custom function to determine if an object is mergeable. * Helps to avoid issues with circular references. * * @param value - The value to check for mergeability. * @returns Whether the value is mergeable or not. * * @example * ```typescript * const canMerge = isMergeable(someValue); * ``` */ const isMergeable = (value) => { return value !== undefined && typeof value === 'object' && !Object.isFrozen(value); }; /** * Validates that the provided blueprints are valid objects. * * This function checks if each blueprint in the provided array is an object, * throwing a SetupError if an invalid blueprint is found. * * @param blueprints - An array of blueprints to validate. * @throws {SetupError} - If any of the provided blueprints are not valid objects. * * @example * ```typescript * validateBlueprints([blueprint1, blueprint2]); * ``` */ const validateBlueprints = (blueprints) => { blueprints.forEach((blueprint, index) => { if (typeof blueprint !== 'object' || blueprint === null) { throw new SetupError(`Invalid blueprint at index ${index}. Expected an object but received ${typeof blueprint}.`); } }); }; /** * Log level enumeration to define possible log levels. */ var LogLevel; (function (LogLevel) { LogLevel["INFO"] = "info"; LogLevel["WARN"] = "warn"; LogLevel["ERROR"] = "error"; LogLevel["DEBUG"] = "debug"; LogLevel["TRACE"] = "trace"; })(LogLevel || (LogLevel = {})); /** ************** End Adapter *************/ /** * Console Logger class. * * This class implements the ILogger interface and uses either the native console object or a custom logging tool. * * @example * ```typescript * const logger = ConsoleLogger.create({ blueprint }); * logger.info('Application started'); * ``` */ class ConsoleLogger { blueprint; /** * Create a new ConsoleLogger instance. * * @param {LoggerOptions} options - Options for creating the ConsoleLogger. * @returns {ConsoleLogger} - A new instance of ConsoleLogger. */ static create(options) { return new this(options); } /** * Constructs a ConsoleLogger instance. * * @param {LoggerOptions} options - Options for creating the ConsoleLogger. */ constructor({ blueprint }) { this.blueprint = blueprint; } /** * Logs informational messages. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ info(message, ...optionalParams) { if (this.shouldLog(LogLevel.INFO)) { console.info(this.formatMessage(message), ...optionalParams); } } /** * Logs debug-level messages, used for debugging purposes. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ debug(message, ...optionalParams) { if (this.shouldLog(LogLevel.DEBUG)) { console.debug(this.formatMessage(message), ...optionalParams); } } /** * Logs warnings, used to indicate potential issues. * * @param {string} message - The warning message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ warn(message, ...optionalParams) { if (this.shouldLog(LogLevel.WARN)) { console.warn(this.formatMessage(message), ...optionalParams); } } /** * Logs errors, used to report errors or exceptions. * * @param {string} message - The error message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ error(message, ...optionalParams) { if (this.shouldLog(LogLevel.ERROR)) { console.error(this.formatMessage(message), ...optionalParams); } } /** * Logs general messages, similar to `info` but less specific. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ log(message, ...optionalParams) { if (this.shouldLog(LogLevel.INFO)) { console.log(this.formatMessage(message), ...optionalParams); } } /** * Determines if the specified log level should be logged based on the current log level setting. * * @param {'error' | 'warn' | 'info' | 'debug' | 'trace'} level - The log level to check. * @returns {boolean} - True if the specified log level should be logged, otherwise false. */ shouldLog(level) { const debug = this.blueprint.get('stone.debug', false); const levels = ['trace', 'debug', 'info', 'warn', 'error']; const appLevel = this.blueprint.get('stone.logger.level', 'info'); return debug || levels.slice(levels.indexOf(appLevel)).includes(level); } /** * Formats the log message by optionally adding a timestamp. * * @param {string} message - The message to format. * @returns {string} - The formatted message. */ formatMessage(message) { if (this.blueprint.get('stone.logger.useTimestamp', false)) { return `[${new Date().toISOString()}] ${message}`; } return message; } } /** * Class representing a Logger for the Stone.js framework. * * Any class that implements the ILogger interface can be used as a logger. * The Logger class provides static methods for logging messages at different levels (info, debug, warn, error). */ /* eslint-disable-next-line @typescript-eslint/no-extraneous-class */ class Logger { static logger; /** * Initializes the logger instance. * * @param {IBlueprint} blueprint - The blueprint to initialize the logger with. */ static init(blueprint) { const resolver = blueprint.get('stone.logger.resolver'); if (isFunctionModule(resolver)) { this.logger = resolver(blueprint); } else { this.logger = ConsoleLogger.create({ blueprint }); } } /** * Returns the current logger instance. * * @returns {ILogger} - The current logger instance. */ static getInstance() { if (isEmpty(this.logger)) { throw new RuntimeError('Logger is not initialized. Call Logger.init(blueprint) before using the logger.'); } return this.logger; } /** * Logs informational messages. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ static info(message, ...optionalParams) { this.getInstance().info(message, ...optionalParams); } /** * Logs debug-level messages, used for debugging purposes. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ static debug(message, ...optionalParams) { this.getInstance().debug(message, ...optionalParams); } /** * Logs warnings, used to indicate potential issues. * * @param {string} message - The warning message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ static warn(message, ...optionalParams) { this.getInstance().warn(message, ...optionalParams); } /** * Logs errors, used to report errors or exceptions. * * @param {string} message - The error message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ static error(message, ...optionalParams) { this.getInstance().error(message, ...optionalParams); } /** * Logs general messages, similar to `info` but less specific. * * @param {string} message - The message to log. * @param {...unknown[]} optionalParams - Optional parameters to log. */ static log(message, ...optionalParams) { this.getInstance().log?.(message, ...optionalParams); } } /** * Class representing an Event. * * @author Mr. Stone <evensstone@gmail.com> */ class Event { /** * The type of the event. */ type; /** * The metadata associated with the event. */ metadata; /** * The source of the event. */ source; /** * The timestamp of the event creation. */ timeStamp; /** * Create an Event. * * @param options - The options to create an Event. */ constructor({ type = '', metadata = {}, source, timeStamp = Date.now() }) { this.type = type; this.source = source; this.timeStamp = timeStamp; this.metadata = isPlainObject(metadata) ? metadata : {}; } /** * Get data from metadata. * * @param key - The key to retrieve from metadata. * @param fallback - The fallback value if the key is not found. * @returns The value associated with the key or the fallback. */ get(key, fallback) { return this.getMetadataValue(key, fallback); } /** * Check if the given value is equal to the specified value. * * @param key - The key to check. * @param value - The value to compare against. * @returns True if the key's value is equal to the specified value, false otherwise. */ is(key, value) { return this.get(key) === value; } /** * Get data from metadata. * * @param key - The key to retrieve from metadata. * @param fallback - The fallback value if the key is not found. * @returns The value associated with the key or the fallback. */ getMetadataValue(key, fallback) { return get(this.metadata, key, fallback); } /** * Add data to metadata. * * @param key - The key or object to add to metadata. * @param value - The value to associate with the key. * @returns This Event instance. */ setMetadataValue(key, value) { Object.entries(isPlainObject(key) ? key : { [key]: value }).forEach(([name, val]) => set(this.metadata, name, val)); return this; } /** * Return a cloned instance. * * @returns A cloned instance of the current class. */ clone() { return Object.assign(Object.create(Object.getPrototypeOf(this)), this); } } /** * EVENT_EMITTER_ALIAS. */ const EVENT_EMITTER_ALIAS = 'eventEmitter'; /** * Class representing an EventEmitter. */ class EventEmitter { listeners; /** * Create an EventEmitter. * * @returns A new EventEmitter instance. */ static create() { return new this(); } /** * Create an EventEmitter. */ constructor() { this.listeners = new Map(); } /** * Registers an event listener for the given event type. * * @param event - The event name or type. * @param handler - The callback to invoke when the event is emitted. */ on(event, handler) { const handlers = this.listeners.get(event); isNotEmpty(handlers) ? handlers.push(handler) : this.listeners.set(event, [handler]); return this; } /** * Removes an event listener for the given event type. * * @param event - The event name or type. * @param handler - The callback to remove. */ off(event, handler) { const handlers = this.listeners.get(event); isNotEmpty(handlers) ? handlers.splice(handlers.indexOf(handler) >>> 0, 1) : this.listeners.set(event, []); return this; } /** * Emits an event, triggering all associated listeners. * * @param event - The event name or an instance of Event. * @param args - Additional arguments to pass to the listeners. */ async emit(event, args) { let eventName; let eventPayload; if (event instanceof Event) { eventName = event.type; eventPayload = event; } else { eventName = event; eventPayload = args; } const handlers = this.listeners.get(eventName); const wilcardHandlers = this.listeners.get('*'); if (isNotEmpty(handlers) && eventPayload !== undefined) { for (const handler of handlers.slice()) { await handler(eventPayload); } } if (isNotEmpty(wilcardHandlers) && eventPayload !== undefined) { for (const handler of wilcardHandlers.slice()) { await handler(eventName, eventPayload); } } } } /** * Class representing an OutgoingResponse. * * @extends Event */ class OutgoingResponse extends Event { /** * OUTGOING_RESPONSE Event name, fires on response to the incoming event. * * @event OutgoingResponse#OUTGOING_RESPONSE */ static OUTGOING_RESPONSE = 'stonejs@outgoing_response'; /** * The original content of the response. */ originalContent; /** * The content of the response. */ _content; /** * The status code of the response. */ _statusCode; /** * The status message of the response. */ _statusMessage; /** * The prepared status of the response. */ prepared; /** * Create an OutgoingResponse. * * @param options - The options to create an OutgoingResponse. * @returns A new OutgoingResponse instance. */ static create(options) { return new this(options); } /** * Create an OutgoingResponse. * * @param options - The options to create an OutgoingResponse. */ constructor({ source, content, metadata = {}, timeStamp = Date.now(), statusCode = undefined, statusMessage = undefined, type = OutgoingResponse.OUTGOING_RESPONSE }) { super({ type, metadata, source, timeStamp }); this.prepared = false; this._content = content; this._statusCode = statusCode; this.originalContent = content; this._statusMessage = statusMessage; } /** * Gets the status code of the outgoing response. * * @returns The status code of the response, or undefined if not set. */ get statusCode() { return this._statusCode; } /** * Gets the status message of the outgoing response. * * @returns The status message of the response, or undefined if not set. */ get statusMessage() { return this._statusMessage; } /** * Gets the content of the outgoing response. * * @returns The content of the outgoing response. */ get content() { return this._content; } /** * Gets the prepared status of the outgoing response. * * @returns The prepared status of the response. */ get isPrepared() { return this.prepared; } /** * Set the status code of the response. * * @param code - The status code. * @param text - Optional status message. * @returns This OutgoingResponse instance. */ setStatus(code, text) { this._statusCode = code; this._statusMessage = text; return this; } /** * Set the content of the response. * * @param content - The content to set. * @returns This OutgoingResponse instance. */ setContent(content) { this._content = content; return this; } /** * Set the prepared status of the response. * * @param prepared - The prepared status to set. * @returns This OutgoingResponse instance. */ setPrepared(prepared) { this.prepared = prepared; return this; } /** * Prepare response before sending it. * * @param _event - The incoming event associated with this response. * @param _container - The container. * @returns This OutgoingResponse instance. */ prepare(_event, _container) { return this.setPrepared(true); } } /** * Custom error for Initialization layer operations. */ class InitializationError extends RuntimeError { constructor(message, options = {}) { super(message, options); this.name = 'InitializationError'; } } /** * Class representing a Kernel. * * The Kernel class is responsible for managing the main lifecycle of the application, including middleware * registration and provider management. It manages the initialization, registration, and booting of the * components required for a fully functional application. * * @author Mr. Stone <evensstone@gmail.com> */ class Kernel { container; blueprint; eventEmitter; providers; registeredProviders; hooks; middleware; resolvedErrorHandlers; resolvedEventHandler; /** * Create a Kernel. * * @param options - Kernel configuration options. * @returns A new Kernel instance. */ static create(options) { return new this(options); } /** * Create a Kernel. * * @param options - Kernel configuration options. */ constructor({ blueprint, container, eventEmitter }) { this.validateOptions({ blueprint, container, eventEmitter }); this.providers = new Set(); this.blueprint = blueprint; this.container = container; this.resolvedErrorHandlers = {}; this.eventEmitter = eventEmitter; this.registeredProviders = new Set(); this.hooks = blueprint.get('stone.lifecycleHooks', {}); this.middleware = blueprint.get('stone.kernel.skipMiddleware', false) ? [] : blueprint.get('stone.kernel.middleware', []); } /** * Populate the context with the given bindings. * The context here is the service container. * Invoke subsequent hooks. * Note: Execution order is important here, never change it. */ async onInit() { this.registerBaseBindings(); await this.runLiveConfigurations(); await this.resolveProviders(); await this.registerProviders(); await this.executeHooks('onInit'); } /** * Boot the providers. * Invoke subsequent hooks. * Note: Execution order is important here, never change it. */ async onHandlingEvent() { await this.bootProviders(); await this.executeHooks('onHandlingEvent'); } /** * Handle Stone IncomingEvent. * * @param event - The Stone incoming event to handle. * @returns The Stone outgoing response. */ async handle(event) { return await this.sendEventThroughDestination(event); } /** * Invoke subsequent hooks after handling the event. */ async onEventHandled() { await this.executeHooks('onEventHandled'); } /** * Invoke subsequent hooks on termination. */ async onTerminate() { await this.executeHooks('onTerminate'); } /** * Send event to the destination. * * @param event - The incoming event. * @returns The prepared response. * @throws InitializationError if no IncomingEvent is provided. */ async sendEventThroughDestination(event) { if (isEmpty(event)) { throw new InitializationError('No IncomingEvent provided.'); } if (isFunctionModule(event.clone)) { this.container.instance('originalEvent', event.clone()); } this.container.instance('event', event).instance('request', event); try { const response = await Pipeline .create(this.makePipelineOptions()) .send(event) .through(...this.middleware) .then(async (ev) => await this.handleEvent(ev)); // We also need to prepare the response here // because the middleware might return a non-prepared response instance. return await this.prepareResponse(event, response); } catch (error) { return await this.handleError(error, event); } } /** * Handle the event. * * @param event - The incoming event. * @returns The outgoing response. */ async handleEvent(event) { await this.executeHooks('onExecutingEventHandler'); try { const response = await this.resolveEventHandler().handle(event); // We need to prepare the response here // because the response middleware might need a prepared response. return await this.prepareResponse(event, response); } catch (error) { return await this.handleError(error, event); } } /** * Handle error. * * @param error - The error to handle. * @param event - The incoming event. * @returns The outgoing response. */ async handleError(error, event) { this.container.instance('error', error); await this.executeHooks('onExecutingErrorHandler'); const response = await this.resolveErrorHandler(error).handle(error, event); return await this.prepareResponse(event, response); } /** * Prepare response before sending * * @param event - The Kernel event. * @param response - The response to prepare. * @returns The prepared response. */ async prepareResponse(event, response) { const validatedResponse = await this.validateAndResolveResponse(response); if (validatedResponse.isPrepared) { return validatedResponse; } this.container.instance('response', validatedResponse); await this.executeHooks('onPreparingResponse'); const preparedResponse = await validatedResponse.prepare(event, this.container); await this.executeHooks('onResponsePrepared'); this.container.instance('response', preparedResponse); return preparedResponse; } /** * Creates pipeline options for the Kernel. * * @returns The pipeline options for configuring middleware. */ makePipelineOptions() { return { hooks: { onPipeProcessed: this.hooks.onKernelMiddlewareProcessed ?? [], onProcessingPipe: this.hooks.onProcessingKernelMiddleware ?? [] }, resolver: (metaPipe) => { if (isClassPipe(metaPipe) || isAliasPipe(metaPipe)) { return this.container.resolve(metaPipe.module, true); } else if (isFactoryPipe(metaPipe)) { return metaPipe.module(this.container); } } }; } /** * Registers the base bindings into the container. * * @private * @returns The Kernel instance. */ registerBaseBindings() { this.container .instance(Config, this.blueprint) .instance(Container, this.container) .instance(Logger, Logger.getInstance()) .instance(EventEmitter, this.eventEmitter) .alias(Config, 'config') .alias(Logger, 'logger') .alias(Config, 'blueprint') .alias(Container, 'container') .alias(EventEmitter, 'events') .alias(EventEmitter, 'eventEmitter'); return this; } /** * Resolves the app event handler from the container. * * @returns The resolved event handler or undefined if not found. * @throws InitializationError if no event handler is found. */ resolveEventHandler() { if (isEmpty(this.resolvedEventHandler)) { const mixedEventHandler = this.blueprint.get('stone.kernel.eventHandler'); if (isMetaClassModule(mixedEventHandler)) { this.resolvedEventHandler = this.container.resolve(mixedEventHandler.module, true); } else if (isMetaFactoryModule(mixedEventHandler)) { this.resolvedEventHandler = { handle: mixedEventHandler.module(this.container) }; } else if (isMetaFunctionModule(mixedEventHandler)) { this.resolvedEventHandler = { handle: mixedEventHandler.module }; } else if (isFunctionModule(mixedEventHandler)) { this.resolvedEventHandler = { handle: mixedEventHandler }; } else { throw new InitializationError('No event handler has been provided.'); } } return this.resolvedEventHandler; } /** * Get the error handler for the given error. * * @param error - The error to get the handler for. * @returns The error handler. * @throws Error if no error handler is found. */ resolveErrorHandler(error) { if (isEmpty(this.resolvedErrorHandlers[error.name])) { const metaErrorHandler = this.blueprint.get(`stone.kernel.errorHandlers.${error.name}`, this.blueprint.get('stone.kernel.errorHandlers.default', {})); if (isMetaClassModule(metaErrorHandler)) { this.resolvedErrorHandlers[error.name] = this.container.resolve(metaErrorHandler.module, true); } else if (isMetaFactoryModule(metaErrorHandler)) { this.resolvedErrorHandlers[error.name] = { handle: metaErrorHandler.module(this.container) }; } else if (isMetaFunctionModule(metaErrorHandler)) { this.resolvedErrorHandlers[error.name] = { handle: metaErrorHandler.module }; } else { throw error; } } return this.resolvedErrorHandlers[error.name]; } /** * Resolves all providers defined in the blueprint. * * @private * @returns The Kernel instance. */ async resolveProviders() { const providers = this.blueprint.get('stone.providers', []); for (const provider of providers) { let resolvedProvider; if (isMetaClassModule(provider)) { resolvedProvider = this.container.resolve(provider.module, true); } else if (isMetaFactoryModule(provider)) { resolvedProvider = provider.module(this.container); } else if (isConstructor(provider)) { resolvedProvider = this.container.resolve(provider, true); } if (resolvedProvider !== undefined && (isEmpty(resolvedProvider.mustSkip) || !(await resolvedProvider.mustSkip()))) { this.providers.add(resolvedProvider); } } } /** * Registers the providers. * * @private * @returns A promise that resolves when all providers are registered. */ async registerProviders() { for (const provider of this.providers) { if (isEmpty(provider.register) || this.registeredProviders.has(provider.constructor.name)) { continue; } await provider.register(); this.registeredProviders.add(provider.constructor.name); } } /** * Boots the providers. * * @private * @returns A promise that resolves when all providers have been booted. */ async bootProviders() { for (const provider of this.providers) { await provider.boot?.(); } } /** * Validate and resolve the response. * * @param returnedValue - The returned value that might be a response. * @returns The validated and resolved response. * @throws InitializationError if the response is invalid or undefined. */ async validateAndResolveResponse(returnedValue) { const responseResolver = this.blueprint.get('stone.kernel.responseResolver'); // Important: Never change this type guard // It is used to check if the response is null or undefined if (returnedValue === undefined || returnedValue === null) { if (isEmpty(responseResolver)) { throw new InitializationError('No response was returned'); } return await responseResolver({}); } if (!(returnedValue instanceof OutgoingResponse)) { if (isEmpty(responseResolver)) { throw new InitializationError('Returned response must be an instance of `OutgoingResponse` or a subclass of it.'); } const valueOptions = returnedValue; const options = (isEmpty(valueOptions?.statusCode) ? { content: returnedValue, statusCode: 200 } : returnedValue); return await responseResolver(options); } return returnedValue; } /** * Run live configurations. * Live configurations are loaded at each request. */ async runLiveConfigurations() { const liveConfigurations = this.blueprint.get('stone.liveConfigurations', []); for (const configuration of liveConfigurations) { if (isMetaClassModule(configuration)) { await this.container.resolve(configuration.module).configure?.(this.blueprint); } else if (isFunctionModule(configuration)) { await configuration(this.blueprint); } } } /** * Execute lifecycle hooks. * * @param name - The hook's name. */ async executeHooks(name) { if (Array.isArray(this.hooks[name]) && name !== 'onKernelMiddlewareProcessed' && name !== 'onProcessingKernelMiddleware') { for (const listener of this.hooks[name]) { await listener(this.container); } } } /** * Validate the Kernel options. * * @param options - The Kernel options to validate. * @throws InitializationError if the options are invalid. */ validateOptions(options) { if (!(options.blueprint instanceof Config)) { throw new InitializationError('Blueprint is required to create a Kernel instance.'); } if (!(options.container instanceof Container)) { throw new InitializationError('Container is required to create a Kernel instance.'); } if (!(options.eventEmitter instanceof EventEmitter)) { throw new InitializationError('EventEmitter is required to create a Kernel instance.'); } } } /** * Constants are defined here to prevent Circular dependency between modules * This pattern must be applied to all Stone libraries or third party libraries. */ /** * A unique symbol key to mark classes as the main application entry point. */ const STONE_APP_KEY = Symbol.for('StoneApp'); /** * A unique symbol key to mark classes as middleware. */ const ADAPTER_MIDDLEWARE_KEY = Symbol.for('AdapterMiddleware'); /** * A unique symbol key to mark classes as middleware. */ const CONFIG_MIDDLEWARE_KEY = Symbol.for('ConfigMiddleware'); /** * A unique symbol used as a key for the configuration metadata. */ const CONFIGURATION_KEY = Symbol.for('Configuration'); /** * A unique symbol used as a key for the error handler metadata. */ const ERROR_HANDLER_KEY = Symbol.for('ErrorHandler'); /** * A unique symbol used as a key for the adapter error handler metadata. */ const ADAPTER_ERROR_HANDLER_KEY = Symbol.for('AdapterErrorHandler'); /** * A unique symbol key to mark classes as listeners. */ const LISTENER_KEY = Symbol.for('Listener'); /** * A unique symbol key to mark classes as middleware. */ const MIDDLEWARE_KEY = Symbol.for('Middleware'); /** * A unique symbol key to mark classes as providers. */ const PROVIDER_KEY = Symbol.for('Provider'); /** * A unique symbol key to mark classes as services. */ const SERVICE_KEY = Symbol.for('Service'); /** * A unique symbol key to mark classes as subscribers. */ const SUBSCRIBER_KEY = Symbol.for('Subscriber'); /** * A unique symbol key to mark classes as the blueprint container. */ const BLUEPRINT_KEY = Symbol.for('blueprint'); /** * A unique symbol key to mark classes as lifecycle hooks. */ const LIFECYCLE_HOOK_KEY = Symbol.for('lifeCycleHook'); /** * A unique symbol for storing and accessing metadata on classes and their members. * This symbol is used by decorators to define and retrieve metadata across modules. */ const MetadataSymbol = (Symbol.metadata !== undefined ? Symbol.metadata : Symbol.for('Symbol.metadata')); /** * Set metadata on a given decorator context. * * @param context - The decorator context where metadata is being set. * @param key - The key for the metadata entry. * @param value - The value of the metadata entry. */ function setMetadata(context, key, value) { context.metadata[key] = value; } /** * Add metadata on a given decorator context. * * @param context - The decorator context where metadata is being added. * @param key - The key for the metadata entry. * @param value - The value of the metadata entry. */ function addMetadata(context, key, value) { context.metadata[key] = [value].concat(context.metadata[key] ?? []); } /** * Check if a class has specific metadata. * * @param Class - The class to check for metadata. * @param key - The key of the metadata to check. * @returns True if the metadata key exists on the class, false otherwise. */ function hasMetadata(Class, key) { return hasMetadataSymbol(Class) && Class[MetadataSymbol]?.[key] !== undefined; } /** * Get the metadata value for a given key from a class. * * @param Class - The class to get the metadata from. * @param key - The key of the metadata to retrieve. * @param fallback - The default value to return if the metadata key is not found. * @returns The metadata value or the default value if the key does not exist. */ function getMetadata(Class, key, fallback) { return (hasMetadataSymbol(Class) ? Class[MetadataSymbol]?.[key] : fallback); } /** * Get all metadata from a class. * * @param Class - The class to get all metadata from. * @param fallback - The default value to return if no metadata is found. * @returns All metadata or the default value if no metadata exists. */ function getAllMetadata(Class, fallback) { return (hasMetadataSymbol(Class) ? Class[MetadataSymbol] : fallback); } /** * Remove a specific metadata entry from a class. * * @param Class - The class to remove metadata from. * @param key - The key of the metadata to remove. */ function removeMetadata(Class, key) { if (hasMetadataSymbol(Class) && Class[MetadataSymbol]?.[key] !== undefined) { Class[MetadataSymbol][key] = undefined; } } /** * Set metadata on a class using a class decorator. * * @param key - The key for the metadata entry. * @param value - The value of the metadata entry. * @returns A class decorator function that sets the metadata. */ function setClassMetadata(key, value) { return classDecoratorLegacyWrapper((_target, context) => { setMetadata(context, key, value); }); } /** * Set metadata on a class method using a method decorator. * * @param key - The key for the metadata entry. * @param value - The value of the metadata entry. * @returns A method decorator function that sets the metadata. */ function setMethodMetadata(key, value) { return methodDecoratorLegacyWrapper((_target, context) => { setMetadata(context, key, value); }); } /** * Set metadata on a class field using a field decorator. * * @param key - The key for the metadata entry. * @param value - The value of the metadata entry. * @returns A field decorator function that sets the metadata. */ function setFieldMetadata(key, value) { return propertyDecoratorLegacyWrapper((_target, context) => { setMetadata(context, key, value); return (initialValue) => initialValue; }); } /** * Add Blueprint on a given decorator context. * * @param _Class - The class to get all metadata from. * @param context - The decorator context where metadata is being set. * @param blueprints - The list of blueprints. */ function addBlueprint(_Class, context, ...blueprints) { context.metadata[BLUEPRINT_KEY] = mergeBlueprints((context.metadata[BLUEPRINT_KEY] ?? { stone: {} }), ...blueprints); } /** * Check if a class has blueprint. * * @param Class - The class to check for metadata. * @returns True if the metadata and BLUEPRINT_KEY keys exist on the class, false otherwise. */ function hasBlueprint(Class) { return hasMetadataSymbol(Class) && Class[MetadataSymbol]?.[BLUEPRINT_KEY] !== undefined; } /** * Get the blueprint value from a class. * * @param Class - The class to get the blueprint from. * @param fallback - The default value to return if the blueprint key is not found. * @returns The blueprint value or the default value if the key does not exist. */ function getBlueprint(Class, fallback) { return (hasMetadataSymbol(Class) ? Class[MetadataSymbol]?.[BLUEPRINT_KEY] : fallback); } /** * Wraps a class decorator to ensure compatibility with both legacy and 2023-11 proposal contexts. * * This wrapper enforces that the decorator is only applied in a valid 2023-11 proposal context * and throws appropriate errors for unsupported usage. * * @template T - The type of the class being decorated. * @param decorator - The class decorator function conforming to the 2023-11 proposal. * @returns A legacy-compatible `ClassDecorator` that works with TypeScript's expectations. * * @throws {SetupError} If the decorator is used outside the 2023-11 context or on invalid targets. */ function classDecoratorLegacyWrapper(decorator) { return (target, potentialContext) => { if (potentialContext !== undefined) { if (potentialContext.kind === 'class') { return decorator(target, potentialContext); } else { throw new SetupError('This decorator can only be applied to classes.'); } } else { throw new SetupError('Class decorators must be used with the 2023-11 decorators proposal. This usage is not supported.'); } }; } /** * Wraps a method decorator to ensure compatibility with both legacy and 2023-11 proposal contexts. * * This wrapper enforces that the decorator is only applied in a valid 2023-11 proposal context * and throws appropriate errors for unsupported usage. * * @template T - The type of the method being decorated. * @param decorator - The method decorator function conforming to the 2023-11 proposal. * @returns A legacy-compatible `MethodDecorator` that works with TypeScript's expectations. * * @throws {SetupError} If the decorator is used outside the 2023-11 context or on invalid targets. */ function methodDecoratorLegacyWrapper(decorator) { return (target, potentialContext, _descriptor) => { if (potentialContext !== undefined) { if (potentialContext.kind === 'method') { return decorator(target, potentialContext); } else { throw new SetupError('This decorator can only be applied to class methods.'); } } else { throw new SetupError('Class method decorators must be used with the 2023-11 decorators proposal. This usage is not supported.'); } }; } /** * Wraps a property decorator to ensure compatibility with both legacy and 2023-11 proposal contexts. * * This wrapper enforces that the decorator is only applied in a valid 2023-11 proposal context * and throws appropriate errors for unsupported usage. * * @param decorator - The property decorator function conforming to the 2023-11 proposal. * @returns A legacy-compatible `PropertyDecorator` that works with TypeScript's expectations. * * @throws {SetupError} If the decorator is used outside the 2023-11 context or on invalid targets. */ function propertyDecoratorLegacyWrapper(decorator) { return (_target, potentialContext) => { if (potentialContext !== undefined) { if (potentialContext.kind === 'field') { return decorator(undefined, potentialContext); } else { throw new SetupError('This decorator can only be applied to class fields.'); } } else { throw new SetupError('Class field decorators must be used with the 2023-11 decorators proposal. This usage is not supported.'); } }; } /** * Type guard to check if a class has metadata. * * @param target - The target class to check. * @returns True if the target has metadata, false otherwise. */ function hasMetadataSymbol(target) { return (isFunctionModule(target) || isObjectLikeModule(target)) && isNotEmpty(target[MetadataSymbol]); } /** * Default logger resolver function. * * This function resolves the logger for the application, using the blueprint configuration. * By default, it creates a `ConsoleLogger` instance with the provided blueprint. * * @param blueprint - The blueprint configuration to use for the logger. * @returns A `ConsoleLogger` instance. */ function defaultLoggerResolver(blueprint) { return ConsoleLogger.create({ blueprint }); } /** * Default response resolver function. * * This function resolves the response for the application, using the options provided. * By default, it creates an `OutgoingResponse` instance with the provided options. * * @param options - The options to create the response. * @returns An outgoing response instance. */ function defaultResponseResolver(options) { return OutgoingResponse.create(options); } /** * Default kernel resolver function. * * This function resolves the kernel for the application, using the blueprint configuration. * It creates a `Kernel` instance with the given blueprint, logger, container, and an event emitter. * * @template U, V * @param blueprint - The blueprint configuration to use for the kernel. * @returns A `Kernel` instance configured with the provided blueprint. */ function defaultKernelResolver(blueprint) { return Kernel.create({ blueprint, container: Container.create(), eventEmitter: EventEmitter.create() }); } /** * **Default Logger Configuration** * * The `logger` constant provides a default setup for the logger. * It includes the following default settings: * * - **Log Level**: `'error'` — Only logs error messages. * - **Color Output**: Disabled — Logs are displayed without color formatting. * - **Timestamp**: Disabled — Timestamps are n