chore: update Angular build cache and TypeScript definitions

This commit is contained in:
2025-09-28 07:13:09 -04:00
parent 53bef252d9
commit f68440656e
13 changed files with 397 additions and 89 deletions
+81 -12
View File
@@ -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;
}
}
+102
View File
@@ -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();
}
}
+65 -3
View File
@@ -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 }) => {