Data Core

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Data Core Library

The Data Core library provides comprehensive data management, state handling, and persistence solutions for our monorepo applications. It offers robust data layer abstractions, caching mechanisms, and reactive state management patterns.

📍 Library Details

🌟 Core Purpose

The Data Core library serves as our data management foundation by providing:

  1. State Management - Reactive state handling and data flow patterns
  2. Data Persistence - Local storage, IndexedDB, and cache management
  3. API Integration - HTTP client abstractions and API utilities
  4. Data Transformation - Data mapping, validation, and serialization
  5. Reactive Patterns - Observable streams and reactive programming utilities

🏗️ Architecture Position

Data Core sits in the Framework Libraries layer of our dependency hierarchy:

Feature Libraries (widgets, editor-ui, shared-ui, auth, etc.)
    ↑ depends on  
Framework Libraries (ux-framework, theme-system, data-core, etc.) ← You are here
    ↑ depends on
Core Libraries (permissions)
    ↑ depends on
Foundation Libraries (core-types, core)

🗄️ Key Features

State Management Service

Reactive state management with observables and signals:

import type { SafeAny } from '@libs-foundational/core-models'; 
import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StateManagementService<T = SafeAny> {
  private state$ = new BehaviorSubject<T>({} as T);
  private loading$ = new BehaviorSubject<boolean>(false);
  private error$ = new BehaviorSubject<Error | null>(null);
  
  // Public observables
  readonly state = this.state$.asObservable();
  readonly loading = this.loading$.asObservable();
  readonly error = this.error$.asObservable();
  
  // Combined state observable
  readonly vm$ = combineLatest([
    this.state,
    this.loading,
    this.error
  ]).pipe(
    map(([state, loading, error]) => ({
      state,
      loading,
      error,
      hasError: !!error,
      isEmpty: !state || Object.keys(state).length === 0
    }))
  );
  
  constructor() {}
  
  // Get current state snapshot
  getCurrentState(): T {
    return this.state$.value;
  }
  
  // Update entire state
  setState(newState: T): void {
    this.state$.next(newState);
    this.clearError();
  }
  
  // Partial state update
  updateState(partialState: Partial<T>): void {
    const currentState = this.getCurrentState();
    const newState = { ...currentState, ...partialState };
    this.setState(newState);
  }
  
  // Update specific property
  updateProperty<K extends keyof T>(key: K, value: T[K]): void {
    this.updateState({ [key]: value } as Partial<T>);
  }
  
  // Reset state to initial value
  resetState(initialState?: T): void {
    this.setState(initialState || {} as T);
  }
  
  // Loading state management
  setLoading(loading: boolean): void {
    this.loading$.next(loading);
    if (loading) {
      this.clearError();
    }
  }
  
  // Error state management
  setError(error: Error | string): void {
    const errorObj = typeof error === 'string' ? new Error(error) : error;
    this.error$.next(errorObj);
    this.setLoading(false);
  }
  
  clearError(): void {
    this.error$.next(null);
  }
  
  // Async action wrapper
  async executeAction<R>(
    action: () => Promise<R>,
    options: ActionOptions = {}
  ): Promise<R> {
    const { showLoading = true, resetErrorOnStart = true } = options;
    
    try {
      if (showLoading) this.setLoading(true);
      if (resetErrorOnStart) this.clearError();
      
      const result = await action();
      
      if (showLoading) this.setLoading(false);
      return result;
    } catch (error) {
      this.setError(error as Error);
      throw error;
    }
  }
  
  // Dispose resources
  dispose(): void {
    this.state$.complete();
    this.loading$.complete();
    this.error$.complete();
  }
}

interface ActionOptions {
  showLoading?: boolean;
  resetErrorOnStart?: boolean;
}

Data Persistence Service

Comprehensive data persistence with multiple storage options:

@Injectable({
  providedIn: 'root'
})
export class DataPersistenceService {
  private memoryCache = new Map<string, CacheEntry>();
  private dbName = 'DataCoreDB';
  private dbVersion = 1;
  private db: IDBDatabase | null = null;
  
  constructor() {
    this.initializeIndexedDB();
  }
  
  // === Memory Cache ===
  
  setCacheValue<T>(key: string, value: T, ttl?: number): void {
    const expiry = ttl ? Date.now() + (ttl * 1000) : undefined;
    this.memoryCache.set(key, {
      value,
      timestamp: Date.now(),
      expiry
    });
  }
  
  getCacheValue<T>(key: string): T | null {
    const entry = this.memoryCache.get(key);
    
    if (!entry) return null;
    
    // Check if expired
    if (entry.expiry && Date.now() > entry.expiry) {
      this.memoryCache.delete(key);
      return null;
    }
    
    return entry.value as T;
  }
  
  clearCache(pattern?: string): void {
    if (pattern) {
      const regex = new RegExp(pattern);
      for (const [key] of this.memoryCache) {
        if (regex.test(key)) {
          this.memoryCache.delete(key);
        }
      }
    } else {
      this.memoryCache.clear();
    }
  }
  
  // === Local Storage ===
  
  setLocalStorage<T>(key: string, value: T): boolean {
    try {
      const serialized = JSON.stringify({
        value,
        timestamp: Date.now()
      });
      localStorage.setItem(key, serialized);
      return true;
    } catch (error) {
      console.error('Failed to set localStorage:', error);
      return false;
    }
  }
  
  getLocalStorage<T>(key: string): T | null {
    try {
      const item = localStorage.getItem(key);
      if (!item) return null;
      
      const parsed = JSON.parse(item);
      return parsed.value as T;
    } catch (error) {
      console.error('Failed to get localStorage:', error);
      return null;
    }
  }
  
  removeLocalStorage(key: string): void {
    localStorage.removeItem(key);
  }
  
  // === Session Storage ===
  
  setSessionStorage<T>(key: string, value: T): boolean {
    try {
      const serialized = JSON.stringify({
        value,
        timestamp: Date.now()
      });
      sessionStorage.setItem(key, serialized);
      return true;
    } catch (error) {
      console.error('Failed to set sessionStorage:', error);
      return false;
    }
  }
  
  getSessionStorage<T>(key: string): T | null {
    try {
      const item = sessionStorage.getItem(key);
      if (!item) return null;
      
      const parsed = JSON.parse(item);
      return parsed.value as T;
    } catch (error) {
      console.error('Failed to get sessionStorage:', error);
      return null;
    }
  }
  
  // === IndexedDB ===
  
  private async initializeIndexedDB(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        // Create object stores
        if (!db.objectStoreNames.contains('data')) {
          const store = db.createObjectStore('data', { keyPath: 'key' });
          store.createIndex('timestamp', 'timestamp');
        }
      };
    });
  }
  
  async setIndexedDB<T>(key: string, value: T): Promise<boolean> {
    if (!this.db) await this.initializeIndexedDB();
    
    return new Promise((resolve) => {
      const transaction = this.db!.transaction(['data'], 'readwrite');
      const store = transaction.objectStore('data');
      
      const data = {
        key,
        value,
        timestamp: Date.now()
      };
      
      const request = store.put(data);
      request.onsuccess = () => resolve(true);
      request.onerror = () => resolve(false);
    });
  }
  
  async getIndexedDB<T>(key: string): Promise<T | null> {
    if (!this.db) await this.initializeIndexedDB();
    
    return new Promise((resolve) => {
      const transaction = this.db!.transaction(['data'], 'readonly');
      const store = transaction.objectStore('data');
      
      const request = store.get(key);
      request.onsuccess = () => {
        const result = request.result;
        resolve(result ? result.value as T : null);
      };
      request.onerror = () => resolve(null);
    });
  }
  
  async removeIndexedDB(key: string): Promise<boolean> {
    if (!this.db) await this.initializeIndexedDB();
    
    return new Promise((resolve) => {
      const transaction = this.db!.transaction(['data'], 'readwrite');
      const store = transaction.objectStore('data');
      
      const request = store.delete(key);
      request.onsuccess = () => resolve(true);
      request.onerror = () => resolve(false);
    });
  }
  
  // === Unified Storage API ===
  
  async store<T>(
    key: string, 
    value: T, 
    options: StorageOptions = {}
  ): Promise<boolean> {
    const { 
      storage = 'localStorage', 
      ttl,
      compress = false 
    } = options;
    
    let processedValue: SafeAny = value;
    
    // Apply compression if requested
    if (compress && typeof value === 'string') {
      // Implement compression logic here
      processedValue = value; // Placeholder
    }
    
    switch (storage) {
      case 'memory':
        this.setCacheValue(key, processedValue, ttl);
        return true;
      case 'localStorage':
        return this.setLocalStorage(key, processedValue);
      case 'sessionStorage':
        return this.setSessionStorage(key, processedValue);
      case 'indexedDB':
        return await this.setIndexedDB(key, processedValue);
      default:
        return false;
    }
  }
  
  async retrieve<T>(
    key: string, 
    options: RetrievalOptions = {}
  ): Promise<T | null> {
    const { storage = 'localStorage', fallbacks = [] } = options;
    
    // Try primary storage
    const primaryResult = await this.getFromStorage<T>(key, storage);
    if (primaryResult !== null) return primaryResult;
    
    // Try fallback storages
    for (const fallbackStorage of fallbacks) {
      const fallbackResult = await this.getFromStorage<T>(key, fallbackStorage);
      if (fallbackResult !== null) return fallbackResult;
    }
    
    return null;
  }
  
  private async getFromStorage<T>(
    key: string, 
    storage: StorageType
  ): Promise<T | null> {
    switch (storage) {
      case 'memory':
        return this.getCacheValue<T>(key);
      case 'localStorage':
        return this.getLocalStorage<T>(key);
      case 'sessionStorage':
        return this.getSessionStorage<T>(key);
      case 'indexedDB':
        return await this.getIndexedDB<T>(key);
      default:
        return null;
    }
  }
}

interface CacheEntry {
  value: SafeAny;
  timestamp: number;
  expiry?: number;
}

interface StorageOptions {
  storage?: StorageType;
  ttl?: number; // Time to live in seconds
  compress?: boolean;
}

interface RetrievalOptions {
  storage?: StorageType;
  fallbacks?: StorageType[];
}

type StorageType = 'memory' | 'localStorage' | 'sessionStorage' | 'indexedDB';

API Client Service

Comprehensive HTTP client with caching and error handling:

@Injectable({
  providedIn: 'root'
})
export class ApiClientService {
  private baseUrl = '';
  private defaultHeaders: Record<string, string> = {
    'Content-Type': 'application/json'
  };
  
  constructor(
    private http: HttpClient,
    private persistence: DataPersistenceService
  ) {}
  
  setBaseUrl(url: string): void {
    this.baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
  }
  
  setDefaultHeader(key: string, value: string): void {
    this.defaultHeaders[key] = value;
  }
  
  removeDefaultHeader(key: string): void {
    delete this.defaultHeaders[key];
  }
  
  // === HTTP Methods ===
  
  get<T>(
    endpoint: string, 
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    return this.request<T>('GET', endpoint, undefined, options);
  }
  
  post<T>(
    endpoint: string, 
    data: SafeAny, 
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    return this.request<T>('POST', endpoint, data, options);
  }
  
  put<T>(
    endpoint: string, 
    data: SafeAny, 
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    return this.request<T>('PUT', endpoint, data, options);
  }
  
  patch<T>(
    endpoint: string, 
    data: SafeAny, 
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    return this.request<T>('PATCH', endpoint, data, options);
  }
  
  delete<T>(
    endpoint: string, 
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    return this.request<T>('DELETE', endpoint, undefined, options);
  }
  
  // === Core Request Method ===
  
  private request<T>(
    method: HttpMethod,
    endpoint: string,
    data?: SafeAny,
    options: RequestOptions = {}
  ): Observable<ApiResponse<T>> {
    const {
      headers = {},
      params = {},
      cache = false,
      cacheTtl = 300, // 5 minutes default
      timeout = 30000, // 30 seconds default
      retries = 0,
      retryDelay = 1000
    } = options;
    
    const url = this.buildUrl(endpoint);
    const requestHeaders = { ...this.defaultHeaders, ...headers };
    const cacheKey = cache ? this.buildCacheKey(method, url, params, data) : null;
    
    // Check cache first for GET requests
    if (cache && method === 'GET' && cacheKey) {
      const cached = this.persistence.getCacheValue<ApiResponse<T>>(cacheKey);
      if (cached) {
        return of(cached);
      }
    }
    
    // Build HTTP options
    const httpOptions: SafeAny = {
      headers: requestHeaders,
      params,
      observe: 'response' as const
    };
    
    if (data) {
      httpOptions.body = data;
    }
    
    // Make request with error handling and retries
    return this.http.request(method, url, httpOptions).pipe(
      timeout(timeout),
      map((response: HttpResponse<T>) => this.processResponse<T>(response)),
      catchError((error) => this.handleError(error)),
      tap((response) => {
        // Cache successful GET responses
        if (cache && method === 'GET' && cacheKey && response.success) {
          this.persistence.setCacheValue(cacheKey, response, cacheTtl);
        }
      }),
      retryWhen(errors => 
        errors.pipe(
          take(retries + 1),
          delayWhen((_, index) => timer(retryDelay * Math.pow(2, index)))
        )
      )
    );
  }
  
  private buildUrl(endpoint: string): string {
    const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
    return `${this.baseUrl}/${cleanEndpoint}`;
  }
  
  private buildCacheKey(
    method: string, 
    url: string, 
    params: SafeAny, 
    data?: SafeAny
  ): string {
    const key = `${method}:${url}`;
    const queryString = new URLSearchParams(params).toString();
    const dataHash = data ? JSON.stringify(data) : '';
    return `${key}:${queryString}:${dataHash}`;
  }
  
  private processResponse<T>(response: HttpResponse<T>): ApiResponse<T> {
    return {
      data: response.body as T,
      success: response.ok,
      status: response.status,
      statusText: response.statusText,
      headers: this.extractHeaders(response.headers),
      timestamp: Date.now()
    };
  }
  
  private extractHeaders(headers: HttpHeaders): Record<string, string> {
    const extracted: Record<string, string> = {};
    headers.keys().forEach(key => {
      extracted[key] = headers.get(key) || '';
    });
    return extracted;
  }
  
  private handleError(error: SafeAny): Observable<ApiResponse<SafeAny>> {
    const apiError: ApiResponse<SafeAny> = {
      data: null,
      success: false,
      status: error.status || 0,
      statusText: error.statusText || 'Unknown Error',
      headers: {},
      timestamp: Date.now(),
      error: {
        message: error.message || 'An error occurred',
        code: error.status || 'UNKNOWN_ERROR',
        details: error.error || error
      }
    };
    
    return of(apiError);
  }
}

interface RequestOptions {
  headers?: Record<string, string>;
  params?: Record<string, SafeAny>;
  cache?: boolean;
  cacheTtl?: number; // Cache time-to-live in seconds
  timeout?: number; // Request timeout in milliseconds
  retries?: number; // Number of retry attempts
  retryDelay?: number; // Delay between retries in milliseconds
}

interface ApiResponse<T> {
  data: T | null;
  success: boolean;
  status: number;
  statusText: string;
  headers: Record<string, string>;
  timestamp: number;
  error?: {
    message: string;
    code: string;
    details: SafeAny;
  };
}

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

📦 Common Usage Patterns

State Management Example

interface UserState {
  users: User[];
  selectedUser: User | null;
  filters: UserFilters;
}

@Injectable({
  providedIn: 'root'
})
export class UserStateService extends StateManagementService<UserState> {
  constructor(private apiClient: ApiClientService) {
    super();
    this.initializeState();
  }
  
  private initializeState(): void {
    this.setState({
      users: [],
      selectedUser: null,
      filters: { status: 'active', role: 'all' }
    });
  }
  
  async loadUsers(): Promise<void> {
    await this.executeAction(async () => {
      const response = await this.apiClient.get<User[]>('/users', {
        cache: true,
        cacheTtl: 300
      }).toPromise();
      
      if (response?.success && response.data) {
        this.updateProperty('users', response.data);
      }
    });
  }
  
  selectUser(user: User): void {
    this.updateProperty('selectedUser', user);
  }
  
  updateFilters(filters: Partial<UserFilters>): void {
    const currentFilters = this.getCurrentState().filters;
    this.updateProperty('filters', { ...currentFilters, ...filters });
  }
  
  // Computed properties
  get filteredUsers$(): Observable<User[]> {
    return this.state.pipe(
      map(state => 
        state.users.filter(user => 
          this.matchesFilters(user, state.filters)
        )
      )
    );
  }
  
  private matchesFilters(user: User, filters: UserFilters): boolean {
    if (filters.status !== 'all' && user.status !== filters.status) {
      return false;
    }
    
    if (filters.role !== 'all' && user.role !== filters.role) {
      return false;
    }
    
    return true;
  }
}

interface User {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  role: 'admin' | 'user' | 'moderator';
}

interface UserFilters {
  status: 'all' | 'active' | 'inactive';
  role: 'all' | 'admin' | 'user' | 'moderator';
}

🛠️ Development

Building the Library

# Build data-core library
npx nx build data-core

# Build with watch mode
npx nx build data-core --watch

Testing

# Run tests
npx nx test data-core

# Run tests with coverage
npx nx test data-core --coverage

# Run linting
npx nx lint data-core

Integration

// Proper import patterns
import { 
  StateManagementService, 
  DataPersistenceService, 
  ApiClientService 
} from '@data-core';

// Use in your application

📋 Best Practices

State Management

  1. Immutable updates - Always create new state objects
  2. Single source of truth - Centralize related state
  3. Reactive patterns - Use observables for data flow
  4. Error handling - Implement comprehensive error states

Data Persistence

  1. Storage strategy - Choose appropriate storage type
  2. Cache invalidation - Implement proper cache expiration
  3. Data compression - Consider compression for large data
  4. Fallback mechanisms - Use multiple storage options

🔗 Dependencies and Usage

Used By

Integration Points

🌟 Key Benefits

Performance

Reliability

Developer Experience

The Data Core library provides the essential data management foundation for building robust, performant, and reliable applications in our monorepo.