@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
JavaScript
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