234 lines
6.8 KiB
TypeScript
234 lines
6.8 KiB
TypeScript
/**
|
|
* useNeighborGraph Hook
|
|
*
|
|
* Builds a navigation graph from page_links to determine which pages
|
|
* are neighbors and should have their assets preloaded.
|
|
*
|
|
* Uses shared asset discovery from lib/assetCache for consistent extraction.
|
|
*/
|
|
|
|
import { useMemo } from 'react';
|
|
import { PRELOAD_CONFIG } from '../config/preload.config';
|
|
import {
|
|
extractElementAssets,
|
|
extractPageBackgroundAssets,
|
|
extractTransitionAssets,
|
|
toPreloadAssetInfo,
|
|
} from '../lib/assetCache';
|
|
import type {
|
|
PreloadPage,
|
|
PreloadPageLink,
|
|
PreloadElement,
|
|
PreloadAssetInfo,
|
|
PreloadNeighborInfo,
|
|
} from '../types/preload';
|
|
|
|
interface UseNeighborGraphOptions {
|
|
pages: PreloadPage[];
|
|
pageLinks: PreloadPageLink[];
|
|
elements: PreloadElement[];
|
|
maxDepth?: number;
|
|
}
|
|
|
|
interface NeighborGraphResult {
|
|
/**
|
|
* Get neighboring page IDs within maxDepth hops
|
|
*/
|
|
getNeighbors: (
|
|
currentPageId: string,
|
|
maxDepth?: number,
|
|
) => PreloadNeighborInfo[];
|
|
|
|
/**
|
|
* Get all assets that should be preloaded for given pages
|
|
*/
|
|
getAssetsForPages: (pageIds: string[]) => PreloadAssetInfo[];
|
|
|
|
/**
|
|
* Get prioritized assets for preloading based on current page
|
|
*/
|
|
getPrioritizedAssets: (
|
|
currentPageId: string,
|
|
maxDepth?: number,
|
|
) => PreloadAssetInfo[];
|
|
|
|
/**
|
|
* Raw adjacency list for debugging
|
|
*/
|
|
adjacencyList: Map<string, string[]>;
|
|
}
|
|
|
|
export function useNeighborGraph(
|
|
options: UseNeighborGraphOptions,
|
|
): NeighborGraphResult {
|
|
const {
|
|
pages,
|
|
pageLinks,
|
|
elements,
|
|
maxDepth = PRELOAD_CONFIG.neighborGraph.maxDepth,
|
|
} = options;
|
|
|
|
// Build adjacency list from page links
|
|
const adjacencyList = useMemo(() => {
|
|
const adj = new Map<string, string[]>();
|
|
|
|
// Initialize all pages
|
|
pages.forEach((page) => {
|
|
adj.set(page.id, []);
|
|
});
|
|
|
|
// Add edges from active page links
|
|
const activeLinks = pageLinks.filter((link) => link.is_active !== false);
|
|
|
|
activeLinks.forEach((link) => {
|
|
if (link.from_pageId && link.to_pageId) {
|
|
const neighbors = adj.get(link.from_pageId) || [];
|
|
if (!neighbors.includes(link.to_pageId)) {
|
|
neighbors.push(link.to_pageId);
|
|
adj.set(link.from_pageId, neighbors);
|
|
}
|
|
}
|
|
});
|
|
|
|
return adj;
|
|
}, [pages, pageLinks]);
|
|
|
|
// BFS to find neighbors within depth
|
|
const getNeighbors = useMemo(() => {
|
|
return (currentPageId: string, depth = maxDepth): PreloadNeighborInfo[] => {
|
|
const visited = new Set<string>();
|
|
const result: PreloadNeighborInfo[] = [];
|
|
const queue: { pageId: string; distance: number }[] = [
|
|
{ pageId: currentPageId, distance: 0 },
|
|
];
|
|
|
|
visited.add(currentPageId);
|
|
|
|
while (queue.length > 0) {
|
|
const item = queue.shift();
|
|
if (!item) break;
|
|
const { pageId, distance } = item;
|
|
|
|
if (distance > 0) {
|
|
result.push({ pageId, distance });
|
|
}
|
|
|
|
if (distance < depth) {
|
|
const neighbors = adjacencyList.get(pageId) || [];
|
|
for (const neighborId of neighbors) {
|
|
if (!visited.has(neighborId)) {
|
|
visited.add(neighborId);
|
|
queue.push({ pageId: neighborId, distance: distance + 1 });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by distance (closest first)
|
|
return result.sort((a, b) => a.distance - b.distance);
|
|
};
|
|
}, [adjacencyList, maxDepth]);
|
|
|
|
// Get assets for a set of pages - uses shared extraction from assetDiscovery
|
|
const getAssetsForPages = useMemo(() => {
|
|
return (pageIds: string[]): PreloadAssetInfo[] => {
|
|
const assets: PreloadAssetInfo[] = [];
|
|
const seenUrls = new Set<string>();
|
|
|
|
pageIds.forEach((pageId) => {
|
|
// Find the page to get its background assets
|
|
const page = pages.find((p) => p.id === pageId);
|
|
if (page) {
|
|
// Use shared extraction for page backgrounds
|
|
const bgAssets = extractPageBackgroundAssets(page);
|
|
bgAssets.forEach((asset) => {
|
|
if (!seenUrls.has(asset.originalUrl)) {
|
|
seenUrls.add(asset.originalUrl);
|
|
assets.push(toPreloadAssetInfo(asset));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Get elements for this page and use shared extraction
|
|
const pageElements = elements.filter((el) => el.pageId === pageId);
|
|
const elementAssets = extractElementAssets(pageElements, pageId);
|
|
elementAssets.forEach((asset) => {
|
|
if (!seenUrls.has(asset.originalUrl)) {
|
|
seenUrls.add(asset.originalUrl);
|
|
assets.push(toPreloadAssetInfo(asset));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Extract transition videos using shared extraction
|
|
pageIds.forEach((pageId) => {
|
|
const transitionAssets = extractTransitionAssets(pageLinks, pageId);
|
|
transitionAssets.forEach((asset) => {
|
|
if (!seenUrls.has(asset.originalUrl)) {
|
|
seenUrls.add(asset.originalUrl);
|
|
assets.push(toPreloadAssetInfo(asset));
|
|
}
|
|
});
|
|
});
|
|
|
|
return assets;
|
|
};
|
|
}, [pages, elements, pageLinks]);
|
|
|
|
// Get prioritized assets for preloading
|
|
const getPrioritizedAssets = useMemo(() => {
|
|
return (currentPageId: string, depth = maxDepth): PreloadAssetInfo[] => {
|
|
// Get current page assets (highest priority)
|
|
const currentPageAssets = getAssetsForPages([currentPageId]).map(
|
|
(asset) => ({
|
|
...asset,
|
|
priority:
|
|
PRELOAD_CONFIG.priority.currentPage +
|
|
(PRELOAD_CONFIG.priority.assetType[asset.assetType] || 0),
|
|
}),
|
|
);
|
|
|
|
// Get neighbor page assets
|
|
const neighbors = getNeighbors(currentPageId, depth);
|
|
const neighborAssets: PreloadAssetInfo[] = [];
|
|
|
|
neighbors.forEach(({ pageId, distance }) => {
|
|
const assets = getAssetsForPages([pageId]);
|
|
assets.forEach((asset) => {
|
|
const basePriority = PRELOAD_CONFIG.priority.neighborBase / distance;
|
|
const typePriority =
|
|
PRELOAD_CONFIG.priority.assetType[asset.assetType] || 0;
|
|
|
|
neighborAssets.push({
|
|
...asset,
|
|
priority: basePriority + typePriority,
|
|
});
|
|
});
|
|
});
|
|
|
|
// Combine and sort by priority (highest first)
|
|
const allAssets = [...currentPageAssets, ...neighborAssets];
|
|
|
|
// Deduplicate by URL, keeping highest priority
|
|
const urlToPriority = new Map<string, PreloadAssetInfo>();
|
|
allAssets.forEach((asset) => {
|
|
const existing = urlToPriority.get(asset.url);
|
|
if (!existing || asset.priority > existing.priority) {
|
|
urlToPriority.set(asset.url, asset);
|
|
}
|
|
});
|
|
|
|
return Array.from(urlToPriority.values()).sort(
|
|
(a, b) => b.priority - a.priority,
|
|
);
|
|
};
|
|
}, [getAssetsForPages, getNeighbors, maxDepth]);
|
|
|
|
return {
|
|
getNeighbors,
|
|
getAssetsForPages,
|
|
getPrioritizedAssets,
|
|
adjacencyList,
|
|
};
|
|
}
|