39948-vm/frontend/src/hooks/useNeighborGraph.ts
2026-04-09 10:06:18 +04:00

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,
};
}