parent
fbc338b8fe
commit
549867821a
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,253 @@
|
|||||||
|
// cerror.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clase de error general para normalizar el manejo de errores en toda la aplicación.
|
||||||
|
*
|
||||||
|
* @extends Error
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* CError proporciona una estructura consistente para errores con:
|
||||||
|
* - **code**: Identificador único del error (útil para i18n y lógica de negocio)
|
||||||
|
* - **message**: Descripción legible del error (opcional)
|
||||||
|
* - **cause**: Error original que causó este error (para stack trace completo)
|
||||||
|
* - **details**: Metadatos adicionales como contexto (serializable para logs)
|
||||||
|
*
|
||||||
|
* Todos los módulos del sistema usan CError para facilitar debugging,
|
||||||
|
* logging estructurado y manejo de errores tipado.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Error básico con código
|
||||||
|
* throw new CError('user_not_found', 'User does not exist');
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con causa original (wrapping de errores)
|
||||||
|
* try {
|
||||||
|
* await database.query('SELECT * FROM users');
|
||||||
|
* } catch (err) {
|
||||||
|
* throw new CError(
|
||||||
|
* 'database_error',
|
||||||
|
* 'Failed to fetch users',
|
||||||
|
* err instanceof Error ? err : undefined
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con detalles contextuales
|
||||||
|
* throw new CError(
|
||||||
|
* 'validation_failed',
|
||||||
|
* 'Invalid user input',
|
||||||
|
* undefined,
|
||||||
|
* { field: 'email', value: 'invalid-email', expected: 'valid email format' }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Manejo tipado de errores
|
||||||
|
* try {
|
||||||
|
* await riskyOperation();
|
||||||
|
* } catch (error) {
|
||||||
|
* if (error instanceof CError) {
|
||||||
|
* switch (error.code) {
|
||||||
|
* case 'timeout':
|
||||||
|
* console.error('Operation timed out:', error.details?.timeout);
|
||||||
|
* break;
|
||||||
|
* case 'unauthorized':
|
||||||
|
* redirectToLogin();
|
||||||
|
* break;
|
||||||
|
* default:
|
||||||
|
* logError(error);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Logging estructurado
|
||||||
|
* function logError(error: CError) {
|
||||||
|
* console.error({
|
||||||
|
* code: error.code,
|
||||||
|
* message: error.message,
|
||||||
|
* details: error.details,
|
||||||
|
* stack: error.stack,
|
||||||
|
* cause: error.cause?.message
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con cprovider (timeout)
|
||||||
|
* // cprovider lanza CError con code 'provider_timeout'
|
||||||
|
* try {
|
||||||
|
* const data = await cprovide.get(slowProvider, 1000);
|
||||||
|
* } catch (error) {
|
||||||
|
* if (error instanceof CError && error.code === 'provider_timeout') {
|
||||||
|
* console.error('Timeout after', error.details?.timeout, 'ms');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class CError extends Error {
|
||||||
|
/**
|
||||||
|
* Código único que identifica el tipo de error.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Usado para:
|
||||||
|
* - Lógica de negocio (switch/if por tipo de error)
|
||||||
|
* - Internacionalización (mapear código a mensaje traducido)
|
||||||
|
* - Métricas y monitoring (agrupar errores por código)
|
||||||
|
* - Testing (verificar errores específicos)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Códigos comunes en el sistema
|
||||||
|
* 'provider_timeout' // cprovider: timeout excedido
|
||||||
|
* 'provider_resolve' // cprovider: error al resolver
|
||||||
|
* 'undefined_provider' // cprovider: provider es undefined
|
||||||
|
* 'invalid_cvalue' // cvalue: tipo inválido
|
||||||
|
* 'memoize_resolution_failed' // cprovider: fallo en memoize
|
||||||
|
*/
|
||||||
|
public code: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error original que causó este error (opcional).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Permite preservar el stack trace completo cuando se wrappean errores.
|
||||||
|
* Útil para debugging y para entender la cadena de errores.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* try {
|
||||||
|
* JSON.parse(invalidJson);
|
||||||
|
* } catch (err) {
|
||||||
|
* throw new CError(
|
||||||
|
* 'parse_error',
|
||||||
|
* 'Failed to parse configuration',
|
||||||
|
* err instanceof Error ? err : undefined
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* // Ahora puedes acceder al SyntaxError original via error.cause
|
||||||
|
*/
|
||||||
|
public cause?: Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadatos adicionales sobre el error (opcional).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Objeto serializable con información contextual del error.
|
||||||
|
* Útil para:
|
||||||
|
* - Debugging (ver valores que causaron el error)
|
||||||
|
* - Logging estructurado (enviar a servicios de monitoreo)
|
||||||
|
* - UI (mostrar información específica al usuario)
|
||||||
|
*
|
||||||
|
* **IMPORTANTE**: Solo incluir datos serializables (JSON-safe).
|
||||||
|
* Evitar referencias circulares, funciones, o datos sensibles.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Contexto de validación
|
||||||
|
* { field: 'email', value: 'test', rule: 'must be valid email' }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Contexto de timeout
|
||||||
|
* { timeout: 5000, operation: 'fetch_user', userId: 123 }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Contexto de tipo inválido
|
||||||
|
* { expected: 'CValue', received: 'object', value: '{"invalid":true}' }
|
||||||
|
*/
|
||||||
|
public details?: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea una nueva instancia de CError.
|
||||||
|
*
|
||||||
|
* @param {string} code - Código único que identifica el tipo de error
|
||||||
|
* @param {string} [message] - Mensaje descriptivo del error (opcional)
|
||||||
|
* @param {Error} [cause] - Error original que causó este error (opcional)
|
||||||
|
* @param {Record<string, any>} [details] - Metadatos adicionales serializables (opcional)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Solo código y mensaje
|
||||||
|
* new CError('not_found', 'Resource not found');
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Código sin mensaje (útil cuando el código es autoexplicativo)
|
||||||
|
* new CError('network_error');
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con todos los parámetros
|
||||||
|
* new CError(
|
||||||
|
* 'validation_error',
|
||||||
|
* 'Email format is invalid',
|
||||||
|
* undefined,
|
||||||
|
* { field: 'email', value: inputEmail, pattern: EMAIL_REGEX }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Wrapping error con causa
|
||||||
|
* try {
|
||||||
|
* await fetch('/api/data');
|
||||||
|
* } catch (err) {
|
||||||
|
* throw new CError(
|
||||||
|
* 'fetch_failed',
|
||||||
|
* 'Could not retrieve data from API',
|
||||||
|
* err instanceof Error ? err : new Error(String(err))
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
code: string,
|
||||||
|
message?: string,
|
||||||
|
cause?: Error,
|
||||||
|
details?: Record<string, any>
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'AppError';
|
||||||
|
this.code = code;
|
||||||
|
this.cause = cause;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* // Pattern: Factory functions para errores comunes
|
||||||
|
* export class ValidationError extends CError {
|
||||||
|
* constructor(field: string, value: any, rule: string) {
|
||||||
|
* super(
|
||||||
|
* 'validation_error',
|
||||||
|
* `Validation failed for field "${field}"`,
|
||||||
|
* undefined,
|
||||||
|
* { field, value, rule }
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Uso
|
||||||
|
* throw new ValidationError('email', userInput, 'must be valid email');
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Pattern: Helper para convertir errores desconocidos
|
||||||
|
* export function toCError(error: unknown, code: string = 'unknown_error'): CError {
|
||||||
|
* if (error instanceof CError) {
|
||||||
|
* return error;
|
||||||
|
* }
|
||||||
|
* if (error instanceof Error) {
|
||||||
|
* return new CError(code, error.message, error);
|
||||||
|
* }
|
||||||
|
* return new CError(code, String(error));
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Uso
|
||||||
|
* try {
|
||||||
|
* riskyOperation();
|
||||||
|
* } catch (err) {
|
||||||
|
* throw toCError(err, 'operation_failed');
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Pattern: Type guard para CError
|
||||||
|
* export function isCError(error: unknown): error is CError {
|
||||||
|
* return error instanceof CError;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Uso
|
||||||
|
* catch (error) {
|
||||||
|
* if (isCError(error) && error.code === 'timeout') {
|
||||||
|
* handleTimeout(error);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
@ -0,0 +1,403 @@
|
|||||||
|
// cprovider.ts
|
||||||
|
import { checkTypeCValue, type CValue } from './cvalue';
|
||||||
|
import { CError } from '$lib/config/cerror';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo unificado que maneja valores síncronos y asíncronos de manera consistente.
|
||||||
|
*
|
||||||
|
* @template T - Tipo del valor a resolver
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Puede ser:
|
||||||
|
* - Un valor directo de tipo T (síncrono)
|
||||||
|
* - Una función que retorna T o Promise<T> (síncrona o asíncrona)
|
||||||
|
* - Una Promise<T> directa
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Valor directo
|
||||||
|
* const directValue: CProvider<number> = 42;
|
||||||
|
*
|
||||||
|
* // Función síncrona
|
||||||
|
* const syncFunction: CProvider<string> = () => 'hello';
|
||||||
|
*
|
||||||
|
* // Función asíncrona
|
||||||
|
* const asyncFunction: CProvider<boolean> = async () => true;
|
||||||
|
*
|
||||||
|
* // Promise directa
|
||||||
|
* const promise: CProvider<number> = Promise.resolve(123);
|
||||||
|
*/
|
||||||
|
export type CProvider<T> = T | (() => T | Promise<T>) | Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider que resuelve a un valor primitivo (string | boolean | number)
|
||||||
|
*/
|
||||||
|
export type CValueProvider = CProvider<CValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider que resuelve a string
|
||||||
|
*/
|
||||||
|
export type StringProvider = CProvider<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider que resuelve a boolean
|
||||||
|
*/
|
||||||
|
export type BooleanProvider = CProvider<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider que resuelve a number
|
||||||
|
*/
|
||||||
|
export type NumberProvider = CProvider<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si un valor es Promise-like (tiene método .then)
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {any} value - Valor a verificar
|
||||||
|
* @returns {value is Promise<T>} true si el valor es una Promise
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function isPromise<T>(value: any): value is Promise<T> {
|
||||||
|
return value && typeof value.then === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica un timeout a una Promise
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {Promise<T>} promise - Promise a la que aplicar timeout
|
||||||
|
* @param {number} timeoutMs - Tiempo máximo en milisegundos
|
||||||
|
* @returns {Promise<T>} Promise que se rechaza si se excede el timeout
|
||||||
|
* @throws {CError} Con código 'provider_timeout' si se excede el tiempo límite
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const result = await withTimeout(
|
||||||
|
* fetch('/api/data'),
|
||||||
|
* 5000 // 5 segundos
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||||
|
const timeoutPromise = new Promise<T>((_, reject) =>
|
||||||
|
setTimeout(() => reject(
|
||||||
|
new CError(
|
||||||
|
'provider_timeout',
|
||||||
|
'Cprovider resolution timed out',
|
||||||
|
undefined,
|
||||||
|
{ timeout: timeoutMs }
|
||||||
|
)
|
||||||
|
), timeoutMs)
|
||||||
|
);
|
||||||
|
return Promise.race([promise, timeoutPromise]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout por defecto en milisegundos
|
||||||
|
* - Producción: 5000ms (5 segundos)
|
||||||
|
* - Desarrollo: Infinity (sin timeout)
|
||||||
|
*/
|
||||||
|
const DEFAULT_TIMEOUT = process.env.NODE_ENV === 'production' ? 5000 : Infinity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API principal para resolver providers de manera uniforme
|
||||||
|
*/
|
||||||
|
interface CProvide {
|
||||||
|
/**
|
||||||
|
* Timeout por defecto para todas las operaciones
|
||||||
|
*/
|
||||||
|
defaultTimeout: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve un provider de forma asíncrona, manejando valores directos,
|
||||||
|
* funciones síncronas/asíncronas y promesas de manera uniforme
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {CProvider<T> | undefined} p - Provider a resolver
|
||||||
|
* @param {number} [timeoutMs=DEFAULT_TIMEOUT] - Timeout opcional en milisegundos
|
||||||
|
* @returns {Promise<T>} Valor resuelto
|
||||||
|
* @throws {CError} 'undefined_provider' si p es undefined
|
||||||
|
* @throws {CError} 'provider_resolve' si falla la resolución
|
||||||
|
* @throws {CError} 'provider_timeout' si se excede el timeout
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Valor directo
|
||||||
|
* const value = await cprovide.get(42); // 42
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Función síncrona
|
||||||
|
* const result = await cprovide.get(() => 'hello'); // 'hello'
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Función asíncrona con timeout
|
||||||
|
* const data = await cprovide.get(
|
||||||
|
* async () => fetchData(),
|
||||||
|
* 2000 // 2 segundos máximo
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Promise directa
|
||||||
|
* const num = await cprovide.get(Promise.resolve(123)); // 123
|
||||||
|
*/
|
||||||
|
get<T>(p: CProvider<T> | undefined, timeoutMs?: number): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve un provider y valida que el resultado sea un CValue válido
|
||||||
|
* (string | boolean | number)
|
||||||
|
*
|
||||||
|
* @param {CValueProvider | undefined} p - Provider a resolver
|
||||||
|
* @param {number} [timeoutMs] - Timeout opcional en milisegundos
|
||||||
|
* @returns {Promise<CValue>} Valor primitivo validado
|
||||||
|
* @throws {CError} Si el valor no es string, boolean o number
|
||||||
|
* @throws {CError} Errores de resolución de get()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const str = await cprovide.asCValue('test'); // 'test'
|
||||||
|
* const num = await cprovide.asCValue(() => 42); // 42
|
||||||
|
* const bool = await cprovide.asCValue(Promise.resolve(true)); // true
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Lanza error - tipo inválido
|
||||||
|
* await cprovide.asCValue({ invalid: true }); // CError
|
||||||
|
*/
|
||||||
|
asCValue(p: CValueProvider | undefined, timeoutMs?: number): Promise<CValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve un provider a string
|
||||||
|
*
|
||||||
|
* @param {StringProvider | undefined} p - Provider a resolver
|
||||||
|
* @param {number} [timeoutMs] - Timeout opcional en milisegundos
|
||||||
|
* @returns {Promise<string>} String resuelto
|
||||||
|
* @throws {CError} Errores de resolución de get()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const name = await cprovide.asString(() => 'John'); // 'John'
|
||||||
|
*/
|
||||||
|
asString(p: StringProvider | undefined, timeoutMs?: number): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve un provider a number
|
||||||
|
*
|
||||||
|
* @param {NumberProvider | undefined} p - Provider a resolver
|
||||||
|
* @param {number} [timeoutMs] - Timeout opcional en milisegundos
|
||||||
|
* @returns {Promise<number>} Number resuelto
|
||||||
|
* @throws {CError} Errores de resolución de get()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const age = await cprovide.asNumber(42); // 42
|
||||||
|
* const computed = await cprovide.asNumber(() => 10 + 32); // 42
|
||||||
|
*/
|
||||||
|
asNumber(p: NumberProvider | undefined, timeoutMs?: number): Promise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve un provider a boolean
|
||||||
|
*
|
||||||
|
* @param {BooleanProvider | undefined} p - Provider a resolver
|
||||||
|
* @param {number} [timeoutMs] - Timeout opcional en milisegundos
|
||||||
|
* @returns {Promise<boolean>} Boolean resuelto
|
||||||
|
* @throws {CError} Errores de resolución de get()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const isActive = await cprovide.asBoolean(true); // true
|
||||||
|
* const hasAccess = await cprovide.asBoolean(async () => checkPermissions()); // true/false
|
||||||
|
*/
|
||||||
|
asBoolean(p: BooleanProvider | undefined, timeoutMs?: number): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea un provider memoizado que cachea el resultado de la primera resolución
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {CProvider<T>} p - Provider a memoizar
|
||||||
|
* @param {Object} [options] - Opciones de memoización
|
||||||
|
* @param {number} [options.timeoutMs] - Timeout para la resolución inicial
|
||||||
|
* @param {number} [options.ttl] - Tiempo de vida del caché en milisegundos
|
||||||
|
* @returns {CProvider<T>} Nuevo provider que retorna el valor cacheado
|
||||||
|
* @throws {CError} 'memoize_resolution_failed' si falla la resolución inicial
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Memoización básica (caché permanente)
|
||||||
|
* const config = cprovide.memoize(async () => {
|
||||||
|
* const res = await fetch('/api/config');
|
||||||
|
* return res.json();
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* const firstCall = await config(); // Hace fetch
|
||||||
|
* const secondCall = await config(); // Retorna de caché
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con TTL (Time To Live)
|
||||||
|
* const userData = cprovide.memoize(
|
||||||
|
* () => fetchUser(),
|
||||||
|
* {
|
||||||
|
* timeoutMs: 3000, // 3 segundos timeout
|
||||||
|
* ttl: 5 * 60 * 1000 // Caché válido por 5 minutos
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
memoize<T>(p: CProvider<T>, options?: { timeoutMs?: number; ttl?: number }): CProvider<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve múltiples providers en paralelo
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {T} providers - Objeto con providers como valores
|
||||||
|
* @param {number} [timeoutMs] - Timeout global opcional en milisegundos
|
||||||
|
* @returns {Promise<{[K in keyof T]: Awaited<ReturnType<CProvide['get']>>}>}
|
||||||
|
* Objeto con los valores resueltos
|
||||||
|
* @throws {CError} Propaga cualquier error de get()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Carga paralela de recursos
|
||||||
|
* const resources = await cprovide.batch({
|
||||||
|
* user: () => fetchUser(),
|
||||||
|
* settings: () => fetchSettings(),
|
||||||
|
* permissions: async () => checkPermissions(),
|
||||||
|
* count: 42
|
||||||
|
* }, 5000);
|
||||||
|
*
|
||||||
|
* console.log(resources);
|
||||||
|
* // { user: {...}, settings: {...}, permissions: true, count: 42 }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Útil para optimizar múltiples llamadas independientes
|
||||||
|
* const data = await cprovide.batch({
|
||||||
|
* id: userId,
|
||||||
|
* name: () => getName(userId),
|
||||||
|
* avatar: fetchAvatar(userId)
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
batch<T extends Record<string, CProvider<any>>>(
|
||||||
|
providers: T,
|
||||||
|
timeoutMs?: number
|
||||||
|
): Promise<{ [K in keyof T]: Awaited<ReturnType<CProvide['get']>> }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API principal para resolver providers de manera uniforme
|
||||||
|
*
|
||||||
|
* @summary
|
||||||
|
* Proporciona métodos para resolver valores síncronos y asíncronos con una API consistente.
|
||||||
|
* Todas las funciones retornan Promise para uniformidad, incluso con valores directos.
|
||||||
|
*
|
||||||
|
* @see {@link CProvide} para la interfaz completa
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Configuración
|
||||||
|
* cprovide.defaultTimeout; // 5000 en producción, Infinity en desarrollo
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Resolución básica
|
||||||
|
* const value = await cprovide.get(42);
|
||||||
|
* const str = await cprovide.get(() => 'hello');
|
||||||
|
* const data = await cprovide.get(fetchData());
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Con validación de tipos
|
||||||
|
* const primitive = await cprovide.asCValue('test'); // string | boolean | number
|
||||||
|
* const name = await cprovide.asString(() => 'John');
|
||||||
|
* const age = await cprovide.asNumber(42);
|
||||||
|
* const active = await cprovide.asBoolean(true);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Memoización para operaciones costosas
|
||||||
|
* const expensiveData = cprovide.memoize(
|
||||||
|
* async () => computeExpensive(),
|
||||||
|
* { ttl: 10 * 60 * 1000 } // 10 minutos
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Resolución en lote (paralelo)
|
||||||
|
* const results = await cprovide.batch({
|
||||||
|
* user: fetchUser(),
|
||||||
|
* posts: fetchPosts(),
|
||||||
|
* stats: () => calculateStats()
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export const cprovide: CProvide = {
|
||||||
|
defaultTimeout: DEFAULT_TIMEOUT,
|
||||||
|
|
||||||
|
async get<T>(p: CProvider<T> | undefined, timeoutMs = DEFAULT_TIMEOUT): Promise<T> {
|
||||||
|
if (p === undefined) {
|
||||||
|
throw new CError('Undefined provider');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (typeof p === 'function') {
|
||||||
|
const result = (p as () => T | Promise<T>)();
|
||||||
|
if (isPromise(result)) {
|
||||||
|
return timeoutMs !== Infinity ? await withTimeout(result, timeoutMs) : await result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (isPromise(p)) {
|
||||||
|
return timeoutMs !== Infinity ? await withTimeout(p, timeoutMs) : await p;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
} catch (cause) {
|
||||||
|
throw new CError(
|
||||||
|
'provider_resolve',
|
||||||
|
`Error resolving provider: ${cause instanceof Error ? cause.message : 'Unknown error'}`,
|
||||||
|
cause instanceof Error ? cause : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async asCValue(p: CValueProvider | undefined, timeoutMs?: number): Promise<CValue> {
|
||||||
|
const value = await cprovide.get<CValue>(p, timeoutMs);
|
||||||
|
checkTypeCValue(value);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async asString(p: StringProvider | undefined, timeoutMs?: number): Promise<string> {
|
||||||
|
return cprovide.get<string>(p, timeoutMs);
|
||||||
|
},
|
||||||
|
|
||||||
|
async asNumber(p: NumberProvider | undefined, timeoutMs?: number): Promise<number> {
|
||||||
|
return cprovide.get<number>(p, timeoutMs);
|
||||||
|
},
|
||||||
|
|
||||||
|
async asBoolean(p: BooleanProvider | undefined, timeoutMs?: number): Promise<boolean> {
|
||||||
|
return cprovide.get<boolean>(p, timeoutMs);
|
||||||
|
},
|
||||||
|
|
||||||
|
memoize<T>(p: CProvider<T>, options: { timeoutMs?: number; ttl?: number } = {}): CProvider<T> {
|
||||||
|
let cache: T | undefined;
|
||||||
|
let resolved = false;
|
||||||
|
let error: CError | undefined;
|
||||||
|
let expiry: number | null = options.ttl ? Date.now() + options.ttl : null;
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
// Verificar validez del caché
|
||||||
|
if (expiry && Date.now() > expiry) {
|
||||||
|
resolved = false;
|
||||||
|
cache = undefined;
|
||||||
|
error = undefined;
|
||||||
|
expiry = options.ttl ? Date.now() + options.ttl : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved) {
|
||||||
|
if (error) throw error;
|
||||||
|
return cache!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cache = await cprovide.get(p, options.timeoutMs);
|
||||||
|
resolved = true;
|
||||||
|
return cache;
|
||||||
|
} catch (err) {
|
||||||
|
throw new CError(
|
||||||
|
'memoize_resolution_failed',
|
||||||
|
'Memoize resolution failed',
|
||||||
|
err instanceof Error ? err : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async batch<T extends Record<string, CProvider<any>>>(
|
||||||
|
providers: T,
|
||||||
|
timeoutMs?: number
|
||||||
|
): Promise<{ [K in keyof T]: Awaited<ReturnType<CProvide['get']>> }> {
|
||||||
|
const entries = await Promise.all(
|
||||||
|
Object.entries(providers).map(async ([key, p]) => [key, await cprovide.get(p as any, timeoutMs)])
|
||||||
|
);
|
||||||
|
return Object.fromEntries(entries) as any;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
// cvalue.ts
|
||||||
|
import { CError } from './cerror';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo unión que representa valores primitivos permitidos en el sistema.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* CValue es la unidad básica de datos primitivos que el sistema puede manejar.
|
||||||
|
* Restringe los valores a tipos simples y serializables.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const name: CValue = 'John'; // string ✓
|
||||||
|
* const age: CValue = 30; // number ✓
|
||||||
|
* const active: CValue = true; // boolean ✓
|
||||||
|
* const obj: CValue = {}; // ✗ Error de compilación
|
||||||
|
* const arr: CValue = []; // ✗ Error de compilación
|
||||||
|
*/
|
||||||
|
export type CValue = string | number | boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si un valor es un CValue válido en runtime.
|
||||||
|
*
|
||||||
|
* @param {any} val - Valor a verificar
|
||||||
|
* @returns {boolean} `true` si el valor es string, number o boolean; `false` en caso contrario
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Esta función realiza una verificación de tipo en tiempo de ejecución.
|
||||||
|
* Es útil para validar datos provenientes de fuentes externas (API, localStorage, etc.)
|
||||||
|
* antes de usarlos como CValue.
|
||||||
|
*
|
||||||
|
* No lanza errores, solo retorna un booleano.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Casos válidos
|
||||||
|
* isCValue('hello'); // true
|
||||||
|
* isCValue(42); // true
|
||||||
|
* isCValue(true); // true
|
||||||
|
* isCValue(false); // true
|
||||||
|
* isCValue(0); // true
|
||||||
|
* isCValue(''); // true (string vacío es válido)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Casos inválidos
|
||||||
|
* isCValue(null); // false
|
||||||
|
* isCValue(undefined); // false
|
||||||
|
* isCValue({}); // false
|
||||||
|
* isCValue([]); // false
|
||||||
|
* isCValue(() => {}); // false
|
||||||
|
* isCValue(Symbol('test')); // false
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Uso práctico con validación
|
||||||
|
* function processValue(input: unknown) {
|
||||||
|
* if (isCValue(input)) {
|
||||||
|
* // TypeScript aún no sabe que input es CValue aquí
|
||||||
|
* // Para type narrowing, usa checkTypeCValue
|
||||||
|
* console.log('Valid CValue:', input);
|
||||||
|
* } else {
|
||||||
|
* console.error('Invalid input');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @see {@link checkTypeCValue} para validación con type assertion
|
||||||
|
*/
|
||||||
|
export function isCValue(val: any): boolean {
|
||||||
|
return typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que un valor sea un CValue y realiza type assertion.
|
||||||
|
*
|
||||||
|
* @param {unknown} value - Valor a validar
|
||||||
|
* @throws {CError} Con código 'invalid_cvalue' si el valor no es un CValue válido
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Esta función es un **type guard con assertion** (asserts). Si no lanza error,
|
||||||
|
* TypeScript garantiza que el valor es de tipo CValue en el scope posterior.
|
||||||
|
*
|
||||||
|
* Útil cuando necesitas validar y estrechar el tipo en una sola operación.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Uso básico con type narrowing automático
|
||||||
|
* function processUnknown(input: unknown){
|
||||||
|
* checkTypeCValue(input);
|
||||||
|
* // Después de esta línea, TypeScript sabe que input es CValue
|
||||||
|
* const upper = input.toUpperCase(); // ✓ Si es string
|
||||||
|
* const sum = input + 10; // ✓ Si es number
|
||||||
|
* }
|
||||||
|
* *
|
||||||
|
* @example
|
||||||
|
* // Validación de datos externos
|
||||||
|
* async function loadConfig() {
|
||||||
|
* const rawValue = localStorage.getItem('config');
|
||||||
|
* const parsed = JSON.parse(rawValue);
|
||||||
|
*
|
||||||
|
* checkTypeCValue(parsed.timeout);
|
||||||
|
* // Ahora parsed.timeout es garantizado CValue
|
||||||
|
* return parsed.timeout;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Manejo de errores
|
||||||
|
* try {
|
||||||
|
* const userInput: unknown = getUserInput();
|
||||||
|
* checkTypeCValue(userInput);
|
||||||
|
* // userInput es CValue aquí
|
||||||
|
* saveToDatabase(userInput);
|
||||||
|
* } catch (error) {
|
||||||
|
* if (error instanceof CError && error.code === 'invalid_cvalue') {
|
||||||
|
* console.error('Invalid value:', error.details?.value);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Validación en array
|
||||||
|
* const values: unknown[] = [1, 'test', true, {}, 'hello'];
|
||||||
|
* const validValues: CValue[] = [];
|
||||||
|
*
|
||||||
|
* for (const val of values) {
|
||||||
|
* try {
|
||||||
|
* checkTypeCValue(val);
|
||||||
|
* validValues.push(val); // val es CValue aquí
|
||||||
|
* } catch {
|
||||||
|
* console.warn('Skipping invalid value');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // En combinación con cprovide
|
||||||
|
* import {cprovide} from './cprovider';
|
||||||
|
*
|
||||||
|
* const config = await cprovide.asCValue(() => fetchConfig());
|
||||||
|
*
|
||||||
|
* // cprovide.asCValue usa checkTypeCValue internamente
|
||||||
|
*
|
||||||
|
* @see {@link isCValue} para verificación sin lanzar errores
|
||||||
|
* @see {@link CValue} para el tipo
|
||||||
|
*/
|
||||||
|
export function checkTypeCValue(value: unknown): asserts value is CValue {
|
||||||
|
if (!isCValue(value)) {
|
||||||
|
throw new CError(
|
||||||
|
'invalid_cvalue',
|
||||||
|
'Invalid cvalue',
|
||||||
|
undefined,
|
||||||
|
{ value: JSON.stringify(value) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
Loading…
Reference in new issue