RSS Git Download  Clone
Raw Blame History 7kB 215 lines
import { Injectable, Inject } from '@angular/core';
import { Subject } from 'rxjs';
import { SocketService } from './socket.service';
import { CommonService } from './common.service';
import { RedisParserService } from './redis-parser.service';
import { RedisStateService } from './redis-state.service';
import { SettingsService } from './settings.service';
import { I18nService } from './i18n.service';
import { NavigationService } from './navigation.service';


/**
 * Main command service — encapsulates Redis operations previously in AngularJS p3xrMain controller.
 *
 * Provides:
 * - selectDatabase(): switch Redis DB index
 * - save(): persist Redis data to disk
 * - refresh(): reload keys and info from server
 * - statistics(): navigate to statistics and refresh
 * - currentDatabase getter/setter with localStorage persistence
 * - addKey(): broadcast new key event
 *
 * Used by main-home-header, main-treecontrol-controls, and the main page component.
 */
@Injectable({ providedIn: 'root' })
export class MainCommandService {

    readonly refreshKey$ = new Subject<void>();
    readonly keyNew$ = new Subject<{ event: Event; node?: any }>();
    readonly keyDelete$ = new Subject<{ key: string; event: Event }>();
    readonly keyRename$ = new Subject<{ key: string; event: Event }>();
    readonly treeControlEnabled$ = new Subject<boolean>();
    readonly mainResizer$ = new Subject<{ drag: boolean }>();
    readonly treeRefresh$ = new Subject<void>();
    readonly consoleEmbeddedResize$ = new Subject<void>();
    readonly consoleActivate$ = new Subject<void>();
    readonly consoleDeactivate$ = new Subject<void>();
    readonly connectRequest$ = new Subject<{ connection: any; disableState?: boolean }>();
    readonly disconnectRequest$ = new Subject<void>();

    constructor(
        @Inject(SocketService) private readonly socket: SocketService,
        @Inject(CommonService) private readonly common: CommonService,
        @Inject(RedisParserService) private readonly redisParser: RedisParserService,
        @Inject(RedisStateService) private readonly state: RedisStateService,
        @Inject(SettingsService) private readonly settings: SettingsService,
        @Inject(I18nService) private readonly i18n: I18nService,
        @Inject(NavigationService) private readonly nav: NavigationService,
    ) {}

    get currentDatabase(): number {
        let db: number | string | undefined | null = this.state.currentDatabase();
        if (db === undefined) {
            db = this.readStorageItem(this.getStorageKey());
        }
        if (db === undefined || db === null) {
            db = 0;
        }
        return Number(db);
    }

    set currentDatabase(value: number) {
        this.state.currentDatabase.set(value);
        const storageKey = this.getStorageKey();
        if (storageKey) {
            try { localStorage.setItem(storageKey, String(value)); } catch {}
        }
    }

    async selectDatabase(dbIndex: number): Promise<void> {
        this.currentDatabase = dbIndex;
        this.socket.stateChanged$.next();
        try {
            this.state.page.set(1);
            await this.socket.request({
                action: 'console',
                payload: { command: `select ${dbIndex}` }
            });
            const strings = this.i18n.strings();
            this.common.toast({
                message: strings.status?.dbChanged?.({ db: dbIndex }) ?? `Database changed to ${dbIndex}`
            });
            await this.statistics();
        } catch (e) {
            this.common.generalHandleError(e);
        } finally {
            this.socket.stateChanged$.next();
        }
    }

    async save(): Promise<void> {
        try {
            const response = await this.socket.request({ action: 'save' });
            const info = this.redisParser.info(response.info);
            this.state.info.set(info);
            const strings = this.i18n.strings();
            this.common.toast({
                message: strings.status?.savedRedis ?? 'Redis saved'
            });
        } catch (e) {
            this.common.generalHandleError(e);
        }
    }

    async statistics(): Promise<void> {
        try {
            this.navigateTo('database.statistics');
            await this.refresh({ force: true });
        } catch (e) {
            this.common.generalHandleError(e);
        }
    }

    private lastRefreshAt = 0;

    async refresh(options: { withoutParent?: boolean; force?: boolean } = {}): Promise<void> {
        // Throttle: skip if last refresh was less than 2s ago
        const now = Date.now();
        if (!options.force && now - this.lastRefreshAt < 2000) return;
        this.lastRefreshAt = now;

        const { withoutParent = false } = options;

        console.time('refresh');

        try {
            const payload: any = {};

            const searchValue = this.state.search();
            if (!this.settings.searchClientSide() &&
                typeof searchValue === 'string' &&
                searchValue.length > 0) {
                if (this.settings.searchStartsWith()) {
                    payload.match = searchValue + '*';
                } else {
                    payload.match = '*' + searchValue + '*';
                }
            }

            const response = await this.socket.request({
                action: 'refresh',
                payload
            });

            this.state.dbsize.set(response.dbsize);
            this.state.redisChanged.set(true);

            await this.common.loadRedisInfoResponse({ response });

            // Tell tree to rebuild with new keys
            this.treeRefresh$.next();

            if (!withoutParent) {
                this.refreshKey$.next();
            }
        } catch (e) {
            this.common.generalHandleError(e);
        } finally {
            console.timeEnd('refresh');
            this.socket.stateChanged$.next();
        }
    }

    addKey(options: { event: Event; node?: any }): void {
        const { event, node } = options;
        event.stopPropagation();
        this.keyNew$.next({ event, node });
    }

    async disconnect(): Promise<void> {
        const conn = this.state.connection();
        const storageKey = this.settings.connectInfoStorageKey;

        // Clear state + storage immediately for instant UI feedback
        if (storageKey) {
            try { localStorage.removeItem(storageKey); } catch {}
        }
        this.state.connection.set(undefined);
        this.state.redisConnections.set({});
        this.state.monitor.set(false);
        this.socket.stateChanged$.next();

        try {
            await this.socket.request({
                action: 'connection-disconnect',
                payload: { connectionId: conn?.id },
            });
        } catch {
            // Ignore — state already cleared
        } finally {
            this.nav.navigateTo('settings');
        }
    }

    navigateTo(state: string, params?: any): void {
        this.nav.navigateTo(state, params);
    }

    // --- Private helpers ---

    private getStorageKey(): string {
        try {
            return this.settings.getStorageKeyCurrentDatabase(this.state.connection()?.id) ?? '';
        } catch {
            return '';
        }
    }

    private readStorageItem(name: string): string | null {
        if (!name) return null;
        try { return localStorage.getItem(name); } catch { return null; }
    }

}