chore: update Angular build cache and TypeScript definitions
This commit is contained in:
parent
53bef252d9
commit
f68440656e
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
@ -20,6 +20,7 @@
|
||||
"angular-calendar": "^0.32.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"highlight.js": "^11.10.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"rxjs": "^7.8.2",
|
||||
|
@ -4,6 +4,7 @@ import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import chokidar from 'chokidar';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@ -15,6 +16,43 @@ const rootDir = path.resolve(__dirname, '..');
|
||||
const distDir = path.join(rootDir, 'dist');
|
||||
const vaultDir = path.join(rootDir, 'vault');
|
||||
|
||||
const vaultEventClients = new Set();
|
||||
|
||||
const registerVaultEventClient = (res) => {
|
||||
const heartbeat = setInterval(() => {
|
||||
try {
|
||||
res.write(':keepalive\n\n');
|
||||
} catch {
|
||||
// Write failures will be handled by the close handler.
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
const client = { res, heartbeat };
|
||||
vaultEventClients.add(client);
|
||||
return client;
|
||||
};
|
||||
|
||||
const unregisterVaultEventClient = (client) => {
|
||||
clearInterval(client.heartbeat);
|
||||
vaultEventClients.delete(client);
|
||||
};
|
||||
|
||||
const broadcastVaultEvent = (payload) => {
|
||||
if (!vaultEventClients.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
||||
for (const client of [...vaultEventClients]) {
|
||||
try {
|
||||
client.res.write(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to notify vault event client:', error);
|
||||
unregisterVaultEventClient(client);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isMarkdownFile = (entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md');
|
||||
|
||||
const normalizeString = (value) => {
|
||||
@ -138,6 +176,44 @@ const isDateWithinRange = (target, start, end) => {
|
||||
return targetTime >= start.getTime() && targetTime <= end.getTime();
|
||||
};
|
||||
|
||||
const vaultWatcher = chokidar.watch(vaultDir, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 250,
|
||||
pollInterval: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const watchedVaultEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
|
||||
|
||||
watchedVaultEvents.forEach((eventName) => {
|
||||
vaultWatcher.on(eventName, (changedPath) => {
|
||||
const relativePath = path.relative(vaultDir, changedPath).replace(/\\/g, '/');
|
||||
broadcastVaultEvent({
|
||||
event: eventName,
|
||||
path: relativePath,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
vaultWatcher.on('ready', () => {
|
||||
broadcastVaultEvent({
|
||||
event: 'ready',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
vaultWatcher.on('error', (error) => {
|
||||
console.error('Vault watcher error:', error);
|
||||
broadcastVaultEvent({
|
||||
event: 'error',
|
||||
message: typeof error?.message === 'string' ? error.message : 'Unknown watcher error',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// Vérifier si le répertoire dist existe
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.warn(`Warning: build directory not found at ${distDir}. Did you run \`npm run build\`?`);
|
||||
@ -151,6 +227,29 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.get('/api/vault/events', (req, res) => {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
});
|
||||
|
||||
res.flushHeaders?.();
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
event: 'connected',
|
||||
timestamp: Date.now(),
|
||||
})}\n\n`,
|
||||
);
|
||||
|
||||
const client = registerVaultEventClient(res);
|
||||
|
||||
req.on('close', () => {
|
||||
unregisterVaultEventClient(client);
|
||||
});
|
||||
});
|
||||
|
||||
// API endpoint pour les données de la voûte (contenu réel)
|
||||
app.get('/api/vault', (req, res) => {
|
||||
try {
|
||||
|
@ -681,54 +681,56 @@
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-1 {
|
||||
font-size: clamp(2.8rem, 4vw, 3.5rem);
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.4rem;
|
||||
border-bottom-width: 3px;
|
||||
font-size: clamp(2.6rem, 3.6vw, 3.1rem);
|
||||
margin-top: 0.55rem;
|
||||
margin-bottom: 0.55rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-2 {
|
||||
font-size: clamp(2.2rem, 3.2vw, 2.8rem);
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1.2rem;
|
||||
border-bottom-width: 2px;
|
||||
font-size: clamp(2.1rem, 3vw, 2.5rem);
|
||||
margin-top: 0.35rem;
|
||||
margin-bottom: 0.35rem;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-3 {
|
||||
font-size: clamp(1.8rem, 2.8vw, 2.2rem);
|
||||
margin-top: 2.6rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(1.7rem, 2.6vw, 2rem);
|
||||
margin-top: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-4 {
|
||||
font-size: clamp(1.5rem, 2.2vw, 1.8rem);
|
||||
margin-top: 2.4rem;
|
||||
margin-bottom: 0.9rem;
|
||||
font-size: clamp(1.45rem, 2.1vw, 1.7rem);
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-5 {
|
||||
font-size: clamp(1.3rem, 1.8vw, 1.4rem);
|
||||
margin-top: 2.2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: clamp(1.25rem, 1.7vw, 1.35rem);
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.22rem;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:host ::ng-deep .md-heading-6 {
|
||||
font-size: clamp(1.1rem, 1.6vw, 1.2rem);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.6rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(1.1rem, 1.5vw, 1.2rem);
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:host ::ng-deep .note-content-area p {
|
||||
margin-top: 0.35rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .md-heading-1 {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@ -740,6 +742,10 @@
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
:host ::ng-deep .prose :where(p, li, blockquote):not(:where(.not-prose *)) {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
:host ::ng-deep .metadata-panel {
|
||||
margin-bottom: 2.2rem;
|
||||
padding: 0.2rem 0 0.2rem 0.8rem;
|
||||
|
@ -49,47 +49,6 @@
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Navigation mobile -->
|
||||
<nav class="sticky bottom-0 z-30 flex w-full items-center justify-around gap-2 border-t border-obs-l-border bg-obs-l-bg-main/95 px-2 py-2 backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-main/95 lg:hidden">
|
||||
<button
|
||||
(click)="setView('files'); toggleSidebarTo(true)"
|
||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
||||
[class.text-obs-l-text-main]="activeView() === 'files'"
|
||||
[class.dark:text-obs-d-text-main]="activeView() === 'files'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
||||
Fichiers
|
||||
</button>
|
||||
<button
|
||||
(click)="setView('search'); toggleSidebarTo(true)"
|
||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
||||
[class.text-obs-l-text-main]="activeView() === 'search'"
|
||||
[class.dark:text-obs-d-text-main]="activeView() === 'search'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
Recherche
|
||||
</button>
|
||||
<button
|
||||
(click)="setView('tags'); toggleSidebarTo(true)"
|
||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
||||
[class.text-obs-l-text-main]="activeView() === 'tags'"
|
||||
[class.dark:text-obs-d-text-main]="activeView() === 'tags'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
||||
Tags
|
||||
</button>
|
||||
<button
|
||||
(click)="setView('calendar'); toggleSidebarTo(true)"
|
||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
||||
[class.text-obs-l-text-main]="activeView() === 'calendar'"
|
||||
[class.dark:text-obs-d-text-main]="activeView() === 'calendar'"
|
||||
[attr.aria-pressed]="activeView() === 'calendar'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
Agenda
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@if (isDesktop() || isSidebarOpen()) {
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-obs-l-border bg-obs-l-bg-secondary shadow-xl transition-transform duration-200 ease-in-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
||||
|
@ -390,6 +390,10 @@ export class AppComponent implements OnDestroy {
|
||||
|
||||
this.vaultService.ensureFolderOpen(note.originalPath);
|
||||
this.selectedNoteId.set(note.id);
|
||||
|
||||
if (!this.isDesktopView() && this.activeView() === 'search') {
|
||||
this.isSidebarOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
handleTagClick(tagName: string): void {
|
||||
|
@ -20,7 +20,7 @@ import { adapterFactory } from 'angular-calendar/date-adapters/date-fns';
|
||||
import { MarkdownCalendarService } from '../../services/markdown-calendar.service';
|
||||
import { FileMetadata } from '../../types';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { Observable, Subject, Subscription } from 'rxjs';
|
||||
|
||||
interface DayBucket {
|
||||
created: FileMetadata[];
|
||||
@ -77,6 +77,7 @@ export class MarkdownCalendarComponent {
|
||||
|
||||
private dragAnchor: Date | null = null;
|
||||
private searchRequestId = 0;
|
||||
private searchSubscription: Subscription | null = null;
|
||||
|
||||
constructor() {
|
||||
this.loadMetadata();
|
||||
@ -193,12 +194,13 @@ export class MarkdownCalendarComponent {
|
||||
|
||||
private runSearch(source$: Observable<FileMetadata[]>): void {
|
||||
const token = ++this.searchRequestId;
|
||||
this.searchSubscription?.unsubscribe();
|
||||
this.isSearching.set(true);
|
||||
this.searchError.set(null);
|
||||
this.searchStateChange.emit('loading');
|
||||
this.searchErrorChange.emit(null);
|
||||
|
||||
source$.subscribe({
|
||||
this.searchSubscription = source$.subscribe({
|
||||
next: (files: FileMetadata[]) => {
|
||||
if (token !== this.searchRequestId) {
|
||||
return;
|
||||
|
@ -24,7 +24,7 @@ interface MetadataEntry {
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="p-8 prose prose-lg dark:prose-invert max-w-none">
|
||||
<div class="p-8 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
|
||||
<div class="!mb-6 pb-2 border-b border-obs-l-border dark:border-obs-d-border">
|
||||
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
|
||||
@if (note().tags.length > 0) {
|
||||
|
@ -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
src/services/vault-events.service.ts
Normal file
102
src/services/vault-events.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 }) => {
|
||||
|
@ -4,5 +4,9 @@ NomDeVoute: IT
|
||||
Description: Page d'accueil de la voute IT
|
||||
tags: [home, accueil]
|
||||
---
|
||||
## TEST
|
||||
bonjour
|
||||
|
||||
# Page d'accueil
|
||||
### allo
|
||||
bonjour
|
||||
alloooo
|
||||
|
@ -68,6 +68,10 @@ Citation en ligne : « > Ceci est une citation »
|
||||
- [ ] Tâche à faire
|
||||
- [X] Tâche terminée
|
||||
|
||||
## Images
|
||||
|
||||
![[Voute_IT.png]]
|
||||
|
||||
## Liens et images
|
||||
|
||||
[Lien vers le site officiel d'Obsidian](https://obsidian.md)
|
||||
@ -209,9 +213,5 @@ Le Markdown peut inclure des notes de bas de page[^1].
|
||||
</details>
|
||||
|
||||
## Sections horizontales
|
||||
|
||||
---
|
||||
|
||||
Fin de la page de test.
|
||||
|
||||
[^1]: Ceci est un exemple de note de bas de page.
|
||||
|
Loading…
x
Reference in New Issue
Block a user