import { basename } from 'path' import { useQueries, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { getEdgeFunctionBody, type EdgeFunctionBodyData, } from '@/data/edge-functions/edge-function-body-query' import { useEdgeFunctionsQuery, type EdgeFunctionsData, } from '@/data/edge-functions/edge-functions-query' import { edgeFunctionsKeys } from '@/data/edge-functions/keys' interface UseEdgeFunctionsDiffProps { currentBranchRef?: string mainBranchRef?: string } export type FileStatus = 'added' | 'removed' | 'modified' | 'unchanged' export interface FileInfo { key: string status: FileStatus } export interface FunctionFileInfo { [functionSlug: string]: FileInfo[] } export interface EdgeFunctionsDiffResult { addedSlugs: string[] removedSlugs: string[] modifiedSlugs: string[] addedBodiesMap: Record removedBodiesMap: Record currentBodiesMap: Record mainBodiesMap: Record functionFileInfo: FunctionFileInfo isLoading: boolean hasChanges: boolean refetchCurrentBranchFunctions: () => void refetchMainBranchFunctions: () => void currentBranchFunctions?: EdgeFunctionsData mainBranchFunctions?: EdgeFunctionsData clearDiffsOptimistically: () => void } // Small helper around path.basename but avoids importing the full Node path lib for the browser bundle const fileKey = (fullPath: string) => basename(fullPath) export const useEdgeFunctionsDiff = ({ currentBranchRef, mainBranchRef, }: UseEdgeFunctionsDiffProps): EdgeFunctionsDiffResult => { const queryClient = useQueryClient() // Fetch edge functions for both branches const { data: currentBranchFunctions, isPending: isCurrentFunctionsLoading, refetch: refetchCurrentBranchFunctions, } = useEdgeFunctionsQuery( { projectRef: currentBranchRef }, { enabled: !!currentBranchRef, refetchOnMount: 'always', staleTime: 30000, // 30 seconds } ) const { data: mainBranchFunctions, isPending: isMainFunctionsLoading, refetch: refetchMainBranchFunctions, } = useEdgeFunctionsQuery( { projectRef: mainBranchRef }, { enabled: !!mainBranchRef, refetchOnMount: 'always', staleTime: 30000, // 30 seconds } ) // Identify added / removed / overlapping functions const { added = [], removed = [], modified = [], } = useMemo(() => { if (!currentBranchFunctions || !mainBranchFunctions) { return { added: [], removed: [], modified: [] } } const currentFuncs = currentBranchFunctions ?? [] const mainFuncs = mainBranchFunctions ?? [] const added = currentFuncs.filter((c) => !mainFuncs.find((m) => m.slug === c.slug)) const removed = mainFuncs.filter((m) => !currentFuncs.find((c) => c.slug === m.slug)) const modified = currentFuncs.filter((c) => mainFuncs.find( (m) => m.slug === c.slug && (m.ezbr_sha256 === undefined || m.ezbr_sha256 !== c.ezbr_sha256) ) ) return { added, removed, modified } }, [currentBranchFunctions, mainBranchFunctions]) const addedSlugs = added.map((f) => f.slug) const removedSlugs = removed.map((f) => f.slug) const maybeModifiedSlugs = modified.map((f) => f.slug) // Fetch function bodies --------------------------------------------------- const currentBodiesQueries = useQueries({ queries: maybeModifiedSlugs.map((slug) => ({ queryKey: ['edge-function-body', currentBranchRef, slug], queryFn: ({ signal }: { signal?: AbortSignal }) => getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal), enabled: !!currentBranchRef, refetchOnMount: 'always' as const, })), }) const mainBodiesQueries = useQueries({ queries: maybeModifiedSlugs.map((slug) => ({ queryKey: ['edge-function-body', mainBranchRef, slug], queryFn: ({ signal }: { signal?: AbortSignal }) => getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal), enabled: !!mainBranchRef, refetchOnMount: 'always' as const, })), }) const addedBodiesQueries = useQueries({ queries: addedSlugs.map((slug) => ({ queryKey: ['edge-function-body', currentBranchRef, slug], queryFn: ({ signal }: { signal?: AbortSignal }) => getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal), enabled: !!currentBranchRef, refetchOnMount: 'always' as const, })), }) const removedBodiesQueries = useQueries({ queries: removedSlugs.map((slug) => ({ queryKey: ['edge-function-body', mainBranchRef, slug], queryFn: ({ signal }: { signal?: AbortSignal }) => getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal), enabled: !!mainBranchRef, refetchOnMount: 'always' as const, })), }) // Flatten loading flags ---------------------------------------------------- const isLoading = [ ...currentBodiesQueries, ...mainBodiesQueries, ...addedBodiesQueries, ...removedBodiesQueries, ].some((q) => q.isLoading) || isCurrentFunctionsLoading || isMainFunctionsLoading // Build lookup maps -------------------------------------------------------- const currentBodiesMap: Record = {} currentBodiesQueries.forEach((q, idx) => { if (q.data) currentBodiesMap[maybeModifiedSlugs[idx]] = q.data }) const mainBodiesMap: Record = {} mainBodiesQueries.forEach((q, idx) => { if (q.data) mainBodiesMap[maybeModifiedSlugs[idx]] = q.data }) const addedBodiesMap: Record = {} addedBodiesQueries.forEach((q, idx) => { if (q.data) addedBodiesMap[addedSlugs[idx]] = q.data }) const removedBodiesMap: Record = {} removedBodiesQueries.forEach((q, idx) => { if (q.data) removedBodiesMap[removedSlugs[idx]] = q.data }) // Determine modified slugs and build file info ----------------------------- const modifiedSlugs: string[] = [] const functionFileInfo: FunctionFileInfo = {} // Process overlapping functions to determine modifications and file info maybeModifiedSlugs.forEach((slug) => { const currentBody = currentBodiesMap[slug] const mainBody = mainBodiesMap[slug] if (!currentBody || !mainBody) return const allFileKeys = new Set( [...currentBody.files, ...mainBody.files].map((f) => fileKey(f.name)) ) const fileInfos: FileInfo[] = [] let hasModifications = false for (const key of allFileKeys) { const currentFile = currentBody.files.find((f) => fileKey(f.name) === key) const mainFile = mainBody.files.find((f) => fileKey(f.name) === key) let status: FileStatus = 'unchanged' if (!currentFile && mainFile) { status = 'removed' hasModifications = true } else if (currentFile && !mainFile) { status = 'added' hasModifications = true } else if (currentFile && mainFile && currentFile.content !== mainFile.content) { status = 'modified' hasModifications = true } fileInfos.push({ key, status }) } if (hasModifications) { modifiedSlugs.push(slug) functionFileInfo[slug] = fileInfos } }) // Add file info for added functions addedSlugs.forEach((slug) => { const body = addedBodiesMap[slug] if (body) { functionFileInfo[slug] = body.files.map((file) => ({ key: fileKey(file.name), status: 'added' as FileStatus, })) } }) // Add file info for removed functions removedSlugs.forEach((slug) => { const body = removedBodiesMap[slug] if (body) { functionFileInfo[slug] = body.files.map((file) => ({ key: fileKey(file.name), status: 'removed' as FileStatus, })) } }) const hasChanges = addedSlugs.length > 0 || removedSlugs.length > 0 || modifiedSlugs.length > 0 const clearDiffsOptimistically = useCallback(() => { if (!currentBranchRef || !mainBranchFunctions) return queryClient.setQueryData(edgeFunctionsKeys.list(currentBranchRef), mainBranchFunctions) mainBranchFunctions.forEach((func) => { const mainBody = mainBodiesMap[func.slug] if (mainBody) { queryClient.setQueryData(['edge-function-body', currentBranchRef, func.slug], mainBody) } }) }, [currentBranchRef, mainBranchFunctions, mainBodiesMap, queryClient]) return { addedSlugs, removedSlugs, modifiedSlugs, addedBodiesMap, removedBodiesMap, currentBodiesMap, mainBodiesMap, functionFileInfo, isLoading, hasChanges, refetchCurrentBranchFunctions, refetchMainBranchFunctions, currentBranchFunctions, mainBranchFunctions, clearDiffsOptimistically, } } export default useEdgeFunctionsDiff