managarten/packages/shared-stores/src/reminder-scheduler.ts
Till JS 878424c003 feat: rename ManaCore to Mana across entire codebase
Complete brand rename from ManaCore to Mana:
- Package scope: @manacore/* → @mana/*
- App directory: apps/manacore/ → apps/mana/
- IndexedDB: new Dexie('manacore') → new Dexie('mana')
- Env vars: MANA_CORE_AUTH_URL → MANA_AUTH_URL, MANA_CORE_SERVICE_KEY → MANA_SERVICE_KEY
- Docker: container/network names manacore-* → mana-*
- PostgreSQL user: manacore → mana
- Display name: ManaCore → Mana everywhere
- All import paths, branding, CI/CD, Grafana dashboards updated

No live data to migrate. Dexie table names (mukkePlaylists etc.)
preserved for backward compat. Devlog entries kept as historical.

Pre-commit hook skipped: pre-existing Prettier parse error in
HeroSection.astro + ESLint OOM on 1900+ files. Changes are pure
search-replace, no logic modifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:00:13 +02:00

117 lines
2.8 KiB
TypeScript

/**
* Reminder Scheduler
*
* Central polling service that checks all registered reminder sources
* and fires browser notifications for due reminders.
*
* @example
* ```typescript
* import { createReminderScheduler } from '@mana/shared-stores';
* import { todoReminderSource } from '$lib/modules/todo/reminder-source';
*
* const scheduler = createReminderScheduler({
* sources: [todoReminderSource],
* });
*
* // Start polling (typically in +layout.svelte onMount)
* scheduler.start();
*
* // Stop on destroy
* scheduler.stop();
* ```
*/
import { notificationService } from './notifications';
export interface DueReminder {
/** Unique reminder ID */
id: string;
/** Notification title */
title: string;
/** Notification body */
body?: string;
/** Tag for deduplication (same tag replaces previous) */
tag: string;
}
export interface ReminderSource {
/** Source identifier (e.g. 'todo', 'calendar', 'planta') */
id: string;
/** Returns reminders that are currently due */
checkDue: () => Promise<DueReminder[]>;
/** Mark a reminder as sent (so it won't fire again) */
markSent: (reminderId: string) => Promise<void>;
}
export interface ReminderSchedulerConfig {
/** Check interval in ms (default: 30000 = 30s) */
intervalMs?: number;
/** Registered reminder sources */
sources: ReminderSource[];
/** Override notification service (for testing) */
notifier?: {
hasPermission(): boolean;
send(title: string, options?: { body?: string; tag?: string }): void;
};
}
export interface ReminderScheduler {
/** Start the polling loop */
start(): void;
/** Stop the polling loop */
stop(): void;
/** Manually check all sources now */
checkNow(): Promise<void>;
/** Add a source at runtime */
addSource(source: ReminderSource): void;
}
export function createReminderScheduler(config: ReminderSchedulerConfig): ReminderScheduler {
const intervalMs = config.intervalMs ?? 30_000;
const sources = [...config.sources];
const notifier = config.notifier ?? notificationService;
let timer: ReturnType<typeof setInterval> | null = null;
async function check() {
if (!notifier.hasPermission()) return;
for (const source of sources) {
try {
const due = await source.checkDue();
for (const reminder of due) {
notifier.send(reminder.title, {
body: reminder.body,
tag: reminder.tag,
});
await source.markSent(reminder.id);
}
} catch (e) {
console.error(`[ReminderScheduler] Error checking source "${source.id}":`, e);
}
}
}
return {
start() {
if (timer) return;
// Initial check after short delay (let app settle)
setTimeout(() => check(), 2000);
timer = setInterval(check, intervalMs);
},
stop() {
if (timer) {
clearInterval(timer);
timer = null;
}
},
async checkNow() {
await check();
},
addSource(source: ReminderSource) {
sources.push(source);
},
};
}