chore: update Angular build cache and TypeScript definitions
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of, shareReplay, finalize, map } from 'rxjs';
|
||||
import { FileMetadata } from '../types';
|
||||
|
||||
@Injectable({
|
||||
@@ -9,23 +9,48 @@ import { FileMetadata } from '../types';
|
||||
export class MarkdownCalendarService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getFilesMetadata(): Observable<FileMetadata[]> {
|
||||
return this.http.get<FileMetadata[]>('/api/files/metadata');
|
||||
private metadataCache: FileMetadata[] | null = null;
|
||||
private metadataRequest$: Observable<FileMetadata[]> | null = null;
|
||||
private readonly parsedDateCache = new Map<string, number | null>();
|
||||
|
||||
getFilesMetadata(forceRefresh = false): Observable<FileMetadata[]> {
|
||||
if (!forceRefresh && this.metadataCache) {
|
||||
return of(this.metadataCache);
|
||||
}
|
||||
|
||||
if (!forceRefresh && this.metadataRequest$) {
|
||||
return this.metadataRequest$;
|
||||
}
|
||||
|
||||
const request$ = this.http
|
||||
.get<FileMetadata[]>('/api/files/metadata')
|
||||
.pipe(
|
||||
map((metadata) => {
|
||||
this.metadataCache = metadata;
|
||||
this.metadataRequest$ = null;
|
||||
this.parsedDateCache.clear();
|
||||
return metadata;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.metadataRequest$ = null;
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
);
|
||||
|
||||
this.metadataRequest$ = request$;
|
||||
return request$;
|
||||
}
|
||||
|
||||
searchFilesByDate(date: Date): Observable<FileMetadata[]> {
|
||||
return this.http.get<FileMetadata[]>('/api/files/by-date', {
|
||||
params: { date: date.toISOString() },
|
||||
});
|
||||
const start = this.startOfDay(date);
|
||||
const end = this.endOfDay(date);
|
||||
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, start, end)));
|
||||
}
|
||||
|
||||
searchFilesByDateRange(startDate: Date, endDate: Date): Observable<FileMetadata[]> {
|
||||
return this.http.get<FileMetadata[]>('/api/files/by-date-range', {
|
||||
params: {
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
},
|
||||
});
|
||||
const rangeStart = this.startOfDay(startDate);
|
||||
const rangeEnd = this.endOfDay(endDate);
|
||||
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, rangeStart, rangeEnd)));
|
||||
}
|
||||
|
||||
groupMetadataByDay(metadata: FileMetadata[]): Map<string, { created: FileMetadata[]; updated: FileMetadata[] }> {
|
||||
@@ -66,4 +91,48 @@ export class MarkdownCalendarService {
|
||||
private appendUnique(list: FileMetadata[], file: FileMetadata): FileMetadata[] {
|
||||
return list.some((item) => item.id === file.id) ? list : list.concat(file);
|
||||
}
|
||||
|
||||
private filterByRange(metadata: FileMetadata[], start: Date, end: Date): FileMetadata[] {
|
||||
const startTime = start.getTime();
|
||||
const endTime = end.getTime();
|
||||
|
||||
return metadata.filter((file) => this.matchesRange(file, startTime, endTime));
|
||||
}
|
||||
|
||||
private matchesRange(file: FileMetadata, startTime: number, endTime: number): boolean {
|
||||
const created = this.getTimeValue(file.createdAt);
|
||||
if (created !== null && created >= startTime && created <= endTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const updated = this.getTimeValue(file.updatedAt);
|
||||
return updated !== null && updated >= startTime && updated <= endTime;
|
||||
}
|
||||
|
||||
private getTimeValue(value: string | undefined | null): number | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.parsedDateCache.has(value)) {
|
||||
return this.parsedDateCache.get(value) ?? null;
|
||||
}
|
||||
|
||||
const time = Date.parse(value);
|
||||
const normalized = Number.isNaN(time) ? null : time;
|
||||
this.parsedDateCache.set(value, normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private startOfDay(date: Date): Date {
|
||||
const start = new Date(date);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return start;
|
||||
}
|
||||
|
||||
private endOfDay(date: Date): Date {
|
||||
const end = new Date(date);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
return end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Injectable, NgZone, OnDestroy } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
export interface VaultEventPayload {
|
||||
event: string;
|
||||
path?: string;
|
||||
timestamp: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class VaultEventsService implements OnDestroy {
|
||||
private readonly eventsSubject = new Subject<VaultEventPayload>();
|
||||
private eventSource: EventSource | null = null;
|
||||
private reconnectTimer: number | null = null;
|
||||
private readonly reconnectDelayMs = 5000;
|
||||
|
||||
constructor(private readonly zone: NgZone) {}
|
||||
|
||||
events$(): Observable<VaultEventPayload> {
|
||||
this.ensureConnection();
|
||||
return this.eventsSubject.asObservable();
|
||||
}
|
||||
|
||||
private ensureConnection(): void {
|
||||
if (this.eventSource || typeof window === 'undefined' || typeof EventSource === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const source = new EventSource('/api/vault/events');
|
||||
this.eventSource = source;
|
||||
|
||||
source.onmessage = (event: MessageEvent<string>) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
|
||||
source.onerror = () => {
|
||||
// Let the browser attempt to reconnect automatically, but make sure we clean up references.
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage(rawData: string): void {
|
||||
if (!rawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: VaultEventPayload | null = null;
|
||||
try {
|
||||
payload = JSON.parse(rawData) as VaultEventPayload;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse vault event payload:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload || typeof payload.event !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.zone.run(() => {
|
||||
this.eventsSubject.next(payload!);
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
|
||||
if (this.reconnectTimer !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.ensureConnection();
|
||||
}, this.reconnectDelayMs);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.reconnectTimer !== null && typeof window !== 'undefined') {
|
||||
window.clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
|
||||
this.eventsSubject.complete();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
import { Injectable, signal, computed, OnDestroy } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Note, VaultNode, GraphData, TagInfo, VaultFolder } from '../types';
|
||||
import { VaultEventsService, VaultEventPayload } from './vault-events.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface VaultApiNote {
|
||||
id: string;
|
||||
@@ -22,7 +24,7 @@ interface VaultApiResponse {
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class VaultService {
|
||||
export class VaultService implements OnDestroy {
|
||||
private notesMap = signal<Map<string, Note>>(new Map());
|
||||
private openFolderPaths = signal(new Set<string>());
|
||||
private initialVaultName = this.resolveVaultName();
|
||||
@@ -123,8 +125,22 @@ export class VaultService {
|
||||
.sort((a, b) => b.count - a.count);
|
||||
});
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
private vaultEventsSubscription: Subscription | null = null;
|
||||
private refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(private http: HttpClient, private vaultEvents: VaultEventsService) {
|
||||
this.refreshNotes();
|
||||
this.observeVaultEvents();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.vaultEventsSubscription?.unsubscribe();
|
||||
this.vaultEventsSubscription = null;
|
||||
|
||||
if (this.refreshTimeoutId !== null) {
|
||||
clearTimeout(this.refreshTimeoutId);
|
||||
this.refreshTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
getNoteById(id: string): Note | undefined {
|
||||
@@ -167,6 +183,52 @@ export class VaultService {
|
||||
this.refreshNotes();
|
||||
}
|
||||
|
||||
private observeVaultEvents(): void {
|
||||
this.vaultEventsSubscription = this.vaultEvents.events$().subscribe({
|
||||
next: (event) => this.handleVaultEvent(event),
|
||||
error: (error) => {
|
||||
console.error('Vault events stream error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleVaultEvent(event: VaultEventPayload): void {
|
||||
if (!event || typeof event.event !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.event) {
|
||||
case 'add':
|
||||
case 'change':
|
||||
case 'unlink':
|
||||
case 'addDir':
|
||||
case 'unlinkDir':
|
||||
this.scheduleRefresh();
|
||||
break;
|
||||
case 'ready':
|
||||
case 'connected':
|
||||
// Initial ready/connected events can trigger a refresh to ensure state is up-to-date.
|
||||
this.scheduleRefresh();
|
||||
break;
|
||||
case 'error':
|
||||
console.error('Vault watcher reported error:', event.message ?? 'Unknown watcher error');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleRefresh(): void {
|
||||
if (this.refreshTimeoutId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshTimeoutId = setTimeout(() => {
|
||||
this.refreshTimeoutId = null;
|
||||
this.refreshNotes();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private refreshNotes() {
|
||||
this.http.get<VaultApiResponse>('/api/vault').subscribe({
|
||||
next: ({ notes }) => {
|
||||
|
||||
Reference in New Issue
Block a user