Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { I18n } from "@/locales/i18n";
import { logger } from "@/lib/logger-client";
import { eventBus, EVENTS } from "@/lib/events";
import { processAudioFileStream } from "@/lib/utils/stream-processor";
import { cacheAudioForOffline, cacheCoverForOffline } from "@/lib/pwa/cache-manager";
import { api } from "@/services";
import type { LibraryOption } from "@/types/models";
import { LibraryBig } from "lucide-react";
Expand Down Expand Up @@ -134,7 +133,9 @@ export function UploadSongForm({ onDrawerClose, targetLibraryId }: UploadSongFor
const songTitle = data.song.title || file.name;

// Cache audio and cover for offline use (non-blocking, fail silently)
// Dynamic import to avoid loading cache-manager in initial bundle
try {
const { cacheAudioForOffline, cacheCoverForOffline } = await import("@/lib/pwa/cache-manager");
await cacheAudioForOffline(songId, file);
if (coverBlob) {
await cacheCoverForOffline(songId, coverBlob);
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/providers/auth-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { startAutoSync, stopAutoSync } from "@/lib/sync/metadata-sync";
import { triggerAutoDownload } from "@/lib/storage/download-manager";
import { useAuthStore } from "@/stores/authStore";
import { initializeEndpoint, isMultiRegionEnabled } from "@/lib/api/multi-region";
import { startIdlePrefetch } from "@/lib/prefetch";

interface AuthProviderProps {
children: React.ReactNode;
Expand All @@ -21,6 +22,11 @@ export function AuthProvider({ children }: AuthProviderProps) {
// Get auth state
const { isAuthenticated, isGuest } = useAuthStore();

// Start idle-time prefetch of heavy modules after initial render
useEffect(() => {
startIdlePrefetch();
}, []);
Comment on lines +25 to +28
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startIdlePrefetch function is called unconditionally in a useEffect with an empty dependency array, which means it will be called every time the AuthProvider mounts. However, the prefetchModule function already has a guard (prefetchedModules Set) that prevents duplicate prefetching. This guard won't work correctly if the module is ever unmounted and remounted because startIdlePrefetch() creates a new setTimeout each time. Consider either: 1) ensuring startIdlePrefetch itself checks if prefetching is already in progress/completed, or 2) documenting that the guard in prefetchModule is intentional and sufficient.

Copilot uses AI. Check for mistakes.

// Initialize multi-region endpoint detection on app startup
// Checks Gateway first, falls back to fastest region if Gateway is down
// Note: initializeEndpoint() is designed to never throw, handles all errors internally
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/lib/prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Prefetch Module - Idle-time module preloading
*
* Uses requestIdleCallback to preload heavy modules during browser idle time.
* This ensures the modules are cached and ready when actually needed,
* without blocking initial page load.
*/

import { logger } from "./logger-client";

// Track which modules have been prefetched
const prefetchedModules = new Set<string>();

/**
* Prefetch a module during browser idle time
* @param moduleLoader - Dynamic import function
* @param name - Module name for logging
*/
function prefetchModule(moduleLoader: () => Promise<unknown>, name: string): void {
if (prefetchedModules.has(name)) return;
prefetchedModules.add(name);

const callback = () => {
moduleLoader()
.then(() => logger.debug(`Prefetched module: ${name}`))
.catch(() => logger.debug(`Failed to prefetch: ${name}`));
};

// Use requestIdleCallback if available, otherwise setTimeout
if ("requestIdleCallback" in window) {
requestIdleCallback(callback, { timeout: 5000 });
Comment on lines +30 to +31
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript does not have built-in type definitions for requestIdleCallback. This code will cause a TypeScript compilation error. You need to add type declarations for requestIdleCallback to the project's type definitions file (e.g., vite-env.d.ts) to properly type this API.

Copilot uses AI. Check for mistakes.
} else {
setTimeout(callback, 2000);
}
}

/**
* Start prefetching heavy modules after initial page load
* Called once after app mounts and becomes interactive
*/
export function startIdlePrefetch(): void {
// Wait a bit for initial render to complete
setTimeout(() => {
// Prefetch cache-manager (contains music-metadata ~100KB)
// Needed for: upload, library delete
prefetchModule(
() => import("@/lib/pwa/cache-manager"),
"cache-manager"
);
}, 3000); // 3 seconds after load
}
13 changes: 9 additions & 4 deletions frontend/src/lib/services/library-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

import { db } from "@/lib/db/schema";
import { logger } from "@/lib/logger-client";
import { getCacheName } from "@/lib/pwa/cache-manager";

// Lazy-load cache-manager to reduce initial bundle size
async function getCacheNameLazy(type: "audio" | "covers"): Promise<string> {
const { getCacheName } = await import("@/lib/pwa/cache-manager");
return getCacheName(type);
}

export interface DeleteProgress {
stage: "librarySongs" | "playlistSongs" | "songs" | "cache" | "library" | "complete";
Expand Down Expand Up @@ -90,7 +95,7 @@ class LibraryService {
message: "Deleting songs and cache",
});

const cache = await caches.open(getCacheName("audio"));
const cache = await caches.open(await getCacheNameLazy("audio"));
let processed = 0;

for (const song of songs) {
Expand Down Expand Up @@ -224,7 +229,7 @@ class LibraryService {
await db.files.delete(fileId);

if (songStreamUrl) {
const cache = await caches.open(getCacheName("audio"));
const cache = await caches.open(await getCacheNameLazy("audio"));
await cache.delete(songStreamUrl);
logger.debug("Cache and file deleted", { songId, fileId });
}
Expand All @@ -240,7 +245,7 @@ class LibraryService {
}
} else if (songStreamUrl) {
// No fileId means unique file (old data), safe to delete
const cache = await caches.open(getCacheName("audio"));
const cache = await caches.open(await getCacheNameLazy("audio"));
await cache.delete(songStreamUrl);
logger.debug("Cache deleted (unique file)", { songId });
}
Expand Down