| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { ref, readonly } from 'vue' |
| import type { RouteLocationNormalized, Router } from 'vue-router' |
|
|
| |
| |
| |
| type ComponentImportFn = () => Promise<unknown> |
|
|
| |
| |
| |
| |
| const PREFETCH_ADJACENCY: Record<string, string[]> = { |
| |
| '/admin/dashboard': ['/admin/accounts', '/admin/users'], |
| '/admin/accounts': ['/admin/dashboard', '/admin/users'], |
| '/admin/users': ['/admin/groups', '/admin/dashboard'], |
| '/admin/groups': ['/admin/subscriptions', '/admin/users'], |
| '/admin/subscriptions': ['/admin/groups', '/admin/redeem'], |
| |
| '/dashboard': ['/keys', '/usage'], |
| '/keys': ['/dashboard', '/usage'], |
| '/usage': ['/keys', '/redeem'], |
| '/redeem': ['/usage', '/profile'], |
| '/profile': ['/dashboard', '/keys'] |
| } |
|
|
| |
| |
| |
| type IdleCallbackHandle = number | ReturnType<typeof setTimeout> |
|
|
| |
| |
| |
| const scheduleIdleCallback = ( |
| callback: IdleRequestCallback, |
| options?: IdleRequestOptions |
| ): IdleCallbackHandle => { |
| if (typeof window.requestIdleCallback === 'function') { |
| return window.requestIdleCallback(callback, options) |
| } |
| return setTimeout(() => { |
| callback({ didTimeout: false, timeRemaining: () => 50 }) |
| }, 1000) |
| } |
|
|
| const cancelScheduledCallback = (handle: IdleCallbackHandle): void => { |
| if (typeof window.cancelIdleCallback === 'function' && typeof handle === 'number') { |
| window.cancelIdleCallback(handle) |
| } else { |
| clearTimeout(handle) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function useRoutePrefetch(router?: Router) { |
| |
| const pendingPrefetchHandle = ref<IdleCallbackHandle | null>(null) |
|
|
| |
| const prefetchedRoutes = ref<Set<string>>(new Set()) |
|
|
| |
| |
| |
| const getComponentImporter = (path: string): ComponentImportFn | null => { |
| if (!router) return null |
|
|
| const routes = router.getRoutes() |
| const route = routes.find((r) => r.path === path) |
|
|
| if (route && route.components?.default) { |
| const component = route.components.default |
| |
| if (typeof component === 'function') { |
| return component as ComponentImportFn |
| } |
| } |
| return null |
| } |
|
|
| |
| |
| |
| const getPrefetchPaths = (route: RouteLocationNormalized): string[] => { |
| return PREFETCH_ADJACENCY[route.path] || [] |
| } |
|
|
| |
| |
| |
| const prefetchComponent = async (importFn: ComponentImportFn): Promise<void> => { |
| try { |
| await importFn() |
| } catch (error) { |
| |
| if (import.meta.env.DEV) { |
| console.debug('[Prefetch] Failed to prefetch component:', error) |
| } |
| } |
| } |
|
|
| |
| |
| |
| const cancelPendingPrefetch = (): void => { |
| if (pendingPrefetchHandle.value !== null) { |
| cancelScheduledCallback(pendingPrefetchHandle.value) |
| pendingPrefetchHandle.value = null |
| } |
| } |
|
|
| |
| |
| |
| const triggerPrefetch = (route: RouteLocationNormalized): void => { |
| cancelPendingPrefetch() |
|
|
| const prefetchPaths = getPrefetchPaths(route) |
| if (prefetchPaths.length === 0) return |
|
|
| pendingPrefetchHandle.value = scheduleIdleCallback( |
| () => { |
| pendingPrefetchHandle.value = null |
|
|
| const routePath = route.path |
| if (prefetchedRoutes.value.has(routePath)) return |
|
|
| |
| const importFns: ComponentImportFn[] = [] |
| for (const path of prefetchPaths) { |
| const importFn = getComponentImporter(path) |
| if (importFn) { |
| importFns.push(importFn) |
| } |
| } |
|
|
| if (importFns.length > 0) { |
| Promise.all(importFns.map(prefetchComponent)).then(() => { |
| prefetchedRoutes.value.add(routePath) |
| }) |
| } |
| }, |
| { timeout: 2000 } |
| ) |
| } |
|
|
| |
| |
| |
| const resetPrefetchState = (): void => { |
| cancelPendingPrefetch() |
| prefetchedRoutes.value.clear() |
| } |
|
|
| |
| |
| |
| const isAdminRoute = (path: string): boolean => { |
| return path.startsWith('/admin') |
| } |
|
|
| |
| |
| |
| const getPrefetchConfig = (route: RouteLocationNormalized): ComponentImportFn[] => { |
| const paths = getPrefetchPaths(route) |
| const importFns: ComponentImportFn[] = [] |
| for (const path of paths) { |
| const importFn = getComponentImporter(path) |
| if (importFn) importFns.push(importFn) |
| } |
| return importFns |
| } |
|
|
| return { |
| prefetchedRoutes: readonly(prefetchedRoutes), |
| triggerPrefetch, |
| cancelPendingPrefetch, |
| resetPrefetchState, |
| _getPrefetchConfig: getPrefetchConfig, |
| _isAdminRoute: isAdminRoute |
| } |
| } |
|
|
| |
| export const _adminPrefetchMap = PREFETCH_ADJACENCY |
| export const _userPrefetchMap = PREFETCH_ADJACENCY |
|
|