Files
familienarchiv/frontend/src/lib/notification/notifications.svelte.ts
2026-05-05 14:22:02 +02:00

131 lines
3.0 KiB
TypeScript

import { type NotificationItem, parseNotificationEvent } from '$lib/notification/notifications';
export type { NotificationItem };
let notifications = $state<NotificationItem[]>([]);
let unreadCount = $state(0);
let eventSource: EventSource | null = null;
let refCount = 0;
let errorCount = 0;
let navigate: (url: string) => void = (url) => {
window.location.href = url;
};
async function fetchNotifications(): Promise<void> {
try {
const res = await fetch('/api/notifications?size=10');
if (res.ok) {
const data = await res.json();
notifications = data.content ?? [];
}
} catch (e) {
console.error('Failed to fetch notifications', e);
}
}
async function fetchUnreadCount(): Promise<void> {
try {
const res = await fetch('/api/notifications/unread-count');
if (res.ok) {
const data = await res.json();
unreadCount = data.count;
}
} catch (e) {
console.error('Failed to fetch unread count', e);
}
}
async function markRead(notification: NotificationItem): Promise<void> {
if (!notification.read) {
try {
await fetch(`/api/notifications/${notification.id}/read`, { method: 'PATCH' });
notification.read = true;
unreadCount = Math.max(0, unreadCount - 1);
} catch (e) {
console.error('Failed to mark notification as read', e);
}
}
}
async function markAllRead(): Promise<void> {
try {
await fetch('/api/notifications/read-all', { method: 'POST' });
for (const n of notifications) {
n.read = true;
}
unreadCount = 0;
} catch (e) {
console.error('Failed to mark all notifications as read', e);
}
}
function init(): void {
refCount += 1;
if (refCount > 1) return;
fetchUnreadCount();
eventSource = new EventSource('/api/notifications/stream');
eventSource.addEventListener('notification', (e) => {
const notification = parseNotificationEvent((e as MessageEvent).data);
if (!notification) return;
notifications = [notification, ...notifications];
if (!notification.read) unreadCount += 1;
});
eventSource.onopen = () => {
fetchUnreadCount();
errorCount = 0;
};
eventSource.onerror = async () => {
if (eventSource?.readyState === EventSource.CLOSED) {
const res = await fetch('/api/notifications/unread-count');
if (res.status === 401) navigate('/login');
return;
}
errorCount += 1;
if (errorCount >= 3) {
eventSource?.close();
eventSource = null;
errorCount = 0;
const res = await fetch('/api/notifications/unread-count');
if (res.status === 401) navigate('/login');
}
};
}
function destroy(): void {
if (refCount === 0) return;
refCount -= 1;
if (refCount === 0) {
eventSource?.close();
eventSource = null;
}
}
export function __resetForTest(): void {
eventSource?.close();
eventSource = null;
refCount = 0;
errorCount = 0;
notifications = [];
unreadCount = 0;
}
export function __setNavigateForTest(fn: (url: string) => void): void {
navigate = fn;
}
export const notificationStore = {
get notifications() {
return notifications;
},
get unreadCount() {
return unreadCount;
},
fetchNotifications,
fetchUnreadCount,
markRead,
markAllRead,
init,
destroy
};