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
- Location:
libs/data-core/ - Dependencies: None (Framework layer)
- Type: Framework Library
- Tags:
npm:public - Purpose: Data management, state handling, and persistence
🌟 Core Purpose
The Data Core library serves as our data management foundation by providing:
- State Management - Reactive state handling and data flow patterns
- Data Persistence - Local storage, IndexedDB, and cache management
- API Integration - HTTP client abstractions and API utilities
- Data Transformation - Data mapping, validation, and serialization
- 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
- Immutable updates - Always create new state objects
- Single source of truth - Centralize related state
- Reactive patterns - Use observables for data flow
- Error handling - Implement comprehensive error states
Data Persistence
- Storage strategy - Choose appropriate storage type
- Cache invalidation - Implement proper cache expiration
- Data compression - Consider compression for large data
- Fallback mechanisms - Use multiple storage options
🔗 Dependencies and Usage
Used By
- UX Test - State management and data testing
- Shared UI - Common data patterns
- Space Map - Data persistence and state
- Editor Web - Content state management
Integration Points
- HTTP Client - Angular HttpClient integration
- RxJS - Reactive programming patterns
- IndexedDB - Browser database storage
- Local Storage - Browser storage APIs
🌟 Key Benefits
Performance
- Efficient caching reduces API calls
- Memory management prevents memory leaks
- Lazy loading of data when needed
- Optimistic updates for better UX
Reliability
- Error recovery with retry mechanisms
- Data persistence across sessions
- Offline support with local storage
- Type safety with TypeScript
Developer Experience
- Reactive patterns with RxJS integration
- Unified API for different storage types
- Comprehensive error handling with detailed information
- Easy testing with mockable services
The Data Core library provides the essential data management foundation for building robust, performant, and reliable applications in our monorepo.