feat(sync): partial sync — lazy collection loading on module visit

Only sync eager apps at startup (manacore, todo, calendar, contacts,
tags, links — needed for dashboard widgets). All other apps are lazy:
their collections sync on first module route visit.

Reduces startup pull requests from ~108 to ~20-30. Lazy apps get
synced when the user navigates to their module via ensureAppSynced().

- Add EAGER_APPS config set in sync.ts
- startAll() only starts pull for eager apps
- ensureAppSynced() starts pull + periodic sync for lazy apps
- Route-based trigger in +layout.svelte $effect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 22:19:20 +02:00
parent f7f5c9eb3a
commit 8ba3c4c10d
2 changed files with 39 additions and 14 deletions

View file

@ -52,6 +52,19 @@ export type SyncStatus = 'idle' | 'syncing' | 'error' | 'offline';
const PUSH_DEBOUNCE = 1000;
const PULL_INTERVAL = 30_000;
const WS_RECONNECT_DELAY = 5000;
/**
* Eager apps are synced at startup (needed for dashboard widgets).
* Lazy apps are synced on first module visit via ensureAppSynced().
*/
const EAGER_APPS = new Set([
'manacore', // User settings, dashboard config
'todo', // Dashboard: tasks today widget
'calendar', // Dashboard: upcoming events widget
'contacts', // Dashboard: favorites widget
'tags', // Global tags used everywhere
'links', // Shared links
]);
// ─── Unified Sync Manager ─────────────────────────────────────
export function createUnifiedSync(serverUrl: string, getToken: () => Promise<string | null>) {
@ -65,6 +78,7 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise<str
// ─── Lifecycle ──────────────────────────────────────────
function startAll(): void {
// Register all channels but only start eager ones immediately
for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
const channel: SyncChannelState = {
appId,
@ -75,9 +89,12 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise<str
};
channels.set(appId, channel);
// Initial pull, then start periodic sync
pull(appId).catch(() => {});
channel.pullTimer = setInterval(() => pull(appId).catch(() => {}), PULL_INTERVAL);
if (EAGER_APPS.has(appId)) {
// Eager: pull now + start periodic sync
pull(appId).catch(() => {});
channel.pullTimer = setInterval(() => pull(appId).catch(() => {}), PULL_INTERVAL);
}
// Lazy apps: no pull until ensureAppSynced() is called
}
// Single unified WebSocket for all apps
@ -457,9 +474,23 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise<str
}
}
/**
* Ensure a lazy app's collections are synced (called on module navigation).
* If already synced (has pullTimer), this is a no-op.
*/
function ensureAppSynced(appId: string): void {
const channel = channels.get(appId);
if (!channel || channel.pullTimer) return; // Already active
// Start sync for this lazy app
pull(appId).catch(() => {});
channel.pullTimer = setInterval(() => pull(appId).catch(() => {}), PULL_INTERVAL);
}
return {
startAll,
stopAll,
ensureAppSynced,
onPendingChange,
get status() {
return status;

View file

@ -140,8 +140,7 @@
// ── Navigation ──────────────────────────────────────────
let baseNavItems = $derived<PillNavItem[]>([
{ href: '/home', label: $_('nav.home'), icon: 'home' },
{ href: '/dashboard', label: $_('nav.dashboard'), icon: 'grid' },
{ href: '/', label: $_('nav.home'), icon: 'home' },
{ href: '/spiral', label: $_('nav.spiral'), icon: 'spiral' },
{ href: '/observatory', label: $_('nav.observatory'), icon: 'eye' },
{ href: '/credits', label: $_('nav.credits'), icon: 'creditCard' },
@ -314,10 +313,11 @@
if (moduleSlug === activeModulePrefix) return;
// Track module usage for funnel analysis
// Track module usage + ensure lazy sync for this module
const moduleName = pathname.split('/')[1];
if (moduleName && authStore.isAuthenticated) {
trackModuleUsed(moduleName);
unifiedSync?.ensureAppSynced(moduleName);
}
const loader = getAdapterLoader(pathname);
@ -337,13 +337,7 @@
});
const spotlightActions: SpotlightAction[] = [
{ id: 'home', label: 'Home', category: 'Navigation', onExecute: () => goto('/home') },
{
id: 'dashboard',
label: 'Dashboard',
category: 'Navigation',
onExecute: () => goto('/dashboard'),
},
{ id: 'home', label: 'Home', category: 'Navigation', onExecute: () => goto('/') },
{
id: 'spiral',
label: 'Mana Spiral',
@ -386,7 +380,7 @@
items={navItems}
currentPath={$page.url.pathname}
appName="ManaCore"
homeRoute="/home"
homeRoute="/"
onLogout={handleSignOut}
onToggleTheme={handleToggleTheme}
{isDark}