All files / app/shared/services/draft-editor draft-editor.service.ts

96.07% Statements 49/51
100% Branches 13/13
100% Functions 9/9
95.91% Lines 47/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 922x 2x 2x 2x   2x     2x 16x 16x 16x   16x     16x 16x 16x     3x 3x 2x 1x   2x   1x 1x         14x 14x 14x 14x 9x   14x       6x 6x 6x 6x 6x 6x 5x   1x         3x 1x 1x         3x       9x   9x     8x 3x   5x         6x 6x 6x 6x              
import { DestroyRef, inject, Injectable } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Draft, DraftInput, DraftStorageService, LoggerService } from '@drevo-web/core';
import { debounceTime, Subject } from 'rxjs';
 
const DRAFT_SAVE_DEBOUNCE_MS = 3000;
 
@Injectable()
export class DraftEditorService {
    private readonly draftStorage = inject(DraftStorageService);
    private readonly logger = inject(LoggerService).withContext('DraftEditorService');
    private readonly destroyRef = inject(DestroyRef);
 
    private readonly contentSubject = new Subject<DraftInput>();
    private lastSavedText: string | undefined;
    private lastPendingInput: DraftInput | undefined;
    private subscriptionInitialized = false;
    private discarded = false;
    private readonly activeRoutes = new Set<string>();
 
    async getDraft(route: string): Promise<Draft | undefined> {
        try {
            const draft = await this.draftStorage.getByRoute(route);
            if (draft) {
                this.logger.info('Draft found', { route, title: draft.title, time: draft.time });
            }
            return draft;
        } catch (error) {
            this.logger.error('Failed to get draft', error);
            return undefined;
        }
    }
 
    onContentChanged(input: DraftInput): void {
        this.discarded = false;
        this.lastPendingInput = input;
        this.activeRoutes.add(input.route);
        if (!this.subscriptionInitialized) {
            this.initSubscription();
        }
        this.contentSubject.next(input);
    }
 
    async discardDraft(route: string): Promise<void> {
        try {
            this.discarded = true;
            this.lastSavedText = undefined;
            this.lastPendingInput = undefined;
            this.activeRoutes.delete(route);
            await this.draftStorage.deleteByRoute(route);
            this.logger.info('Draft discarded', { route });
        } catch (error) {
            this.logger.error('Failed to discard draft', error);
        }
    }
 
    flush(): void {
        if (this.lastPendingInput && !this.discarded && this.lastPendingInput.text !== this.lastSavedText) {
            this.saveDraft(this.lastPendingInput);
            this.lastPendingInput = undefined;
        }
    }
 
    hasActiveSession(route: string): boolean {
        return this.activeRoutes.has(route);
    }
 
    private initSubscription(): void {
        this.subscriptionInitialized = true;
 
        this.contentSubject
            .pipe(debounceTime(DRAFT_SAVE_DEBOUNCE_MS), takeUntilDestroyed(this.destroyRef))
            .subscribe(input => {
                if (this.discarded || input.text === this.lastSavedText) {
                    return;
                }
                this.saveDraft(input);
            });
    }
 
    private async saveDraft(input: DraftInput): Promise<void> {
        try {
            this.lastSavedText = input.text;
            await this.draftStorage.save(input);
            this.logger.debug('Draft saved', { route: input.route, length: input.text.length });
        } catch (error) {
            this.lastSavedText = undefined;
            this.logger.error('Failed to save draft', error);
        }
    }
}