import { Injectable, Inject, signal, computed } from '@angular/core'; import { SettingsService } from './settings.service'; declare const P3XR_API_PORT: number; /** * Runtime state service using Angular signals. * * Single source of truth for all Redis UI runtime state. No global object dependency. */ @Injectable({ providedIn: 'root' }) export class RedisStateService { // --- Writable signals for runtime state --- readonly theme = signal(undefined); readonly connection = signal(undefined); readonly currentDatabase = signal(undefined); readonly databaseIndexes = signal([0]); readonly connections = signal({ list: [] }); readonly redisConnections = signal>({}); readonly keysRaw = signal([]); readonly keysInfo = signal(undefined); readonly search = signal(this.getStoredSearch()); readonly page = signal(1); readonly info = signal(undefined); readonly dbsize = signal(undefined); readonly redisChanged = signal(false); readonly failed = signal(false); readonly monitor = signal(false); readonly monitorPattern = signal('*'); readonly commands = signal([]); readonly commandsMeta = signal>({}); readonly cfg = signal(undefined); readonly version = signal(undefined); readonly modules = signal([]); readonly hasRediSearch = signal(false); readonly hasReJSON = signal(false); readonly hasTimeSeries = signal(false); readonly reducedFunctions = signal(false); readonly keysInfoFetchedAt = signal(Date.now()); // --- Computed values --- readonly themeLayout = computed(() => { const t = this.theme(); return t ? t + 'Layout' : undefined; }); readonly themeCommon = computed(() => { const t = this.theme(); return t ? t + 'Common' : undefined; }); readonly filteredKeys = computed(() => { let keys = this.keysRaw().slice(); const search = this.search(); const settings = this.settings; // Apply client-side search filter if (settings.searchClientSide() && typeof search === 'string' && search.length > 0) { if (settings.searchStartsWith()) { keys = keys.filter((key) => key.startsWith(search)); } else { keys = keys.filter((key) => key.includes(search)); } } return keys; }); readonly paginatedKeys = computed(() => { const keys = this.filteredKeys(); const pageSize = this.settings.pageCount(); if (keys.length <= pageSize) { return keys; } const start = (this.page() - 1) * pageSize; return keys.slice(start, start + pageSize); }); readonly pages = computed(() => { return Math.ceil(this.filteredKeys().length / this.settings.pageCount()); }); // --- API host (computed once at startup) --- readonly apiHost: string = (() => { const apiUrl = new URL(location.toString()); if ((globalThis as any).p3xrDevMode === true) { const apiPort = typeof P3XR_API_PORT !== 'undefined' ? P3XR_API_PORT : 7843; return `http://${apiUrl.hostname}:${apiPort}`; } return `${apiUrl.protocol}//${apiUrl.host}`; })(); constructor(@Inject(SettingsService) private settings: SettingsService) {} /** * Resets connections to default state. */ resetConnections(): void { this.connections.set({ list: [] }); } private getStoredSearch(): string { try { return localStorage.getItem('p3xr-state-search') ?? ''; } catch { return ''; } } }