diff --git a/apps/app-frontend/src/store/install.js b/apps/app-frontend/src/store/install.js index 2df6da020b..3e836c4368 100644 --- a/apps/app-frontend/src/store/install.js +++ b/apps/app-frontend/src/store/install.js @@ -2,7 +2,7 @@ import dayjs from 'dayjs' -import { get_project, get_version_many } from '@/helpers/cache.js' +import { get_project, get_version, get_version_many } from '@/helpers/cache.js' import { add_project_from_version, check_installed } from '@/helpers/profile.js' import { add_server_to_profile, @@ -49,33 +49,138 @@ export const isVersionCompatible = (version, project, instance) => { } export const installVersionDependencies = async (profile, version, onDepInstalling) => { - for (const dep of version.dependencies) { - if (dep.dependency_type !== 'required') continue - // disallow fabric api install on quilt - if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue + const projectNames = new Map() + const storeProjectName = (p) => { + if (p?.id && p.title) projectNames.set(p.id, p.title) + } + + const visitedVersions = new Set() + const announcedProjects = new Set() + const queuedVersionIds = new Set() + const queuedProjectVersions = new Map() + const queuedInstalls = [] + const installedProjectCache = new Map() + + const isProjectInstalled = async (projectId) => { + if (!projectId) return false + if (installedProjectCache.has(projectId)) { + return installedProjectCache.get(projectId) + } + const installed = await check_installed(profile.path, projectId) + installedProjectCache.set(projectId, installed) + return installed + } + + const queueInstall = async (projectId, resolvedVersion) => { + if (!resolvedVersion?.id) return false + + const versionId = resolvedVersion.id + const resolvedProjectId = projectId ?? resolvedVersion.project_id ?? null + + if (resolvedProjectId) { + if (await isProjectInstalled(resolvedProjectId)) return false + + const existingVersionId = queuedProjectVersions.get(resolvedProjectId) + if (existingVersionId && existingVersionId !== versionId) return false + if (existingVersionId === versionId) return false + } + + if (queuedVersionIds.has(versionId)) return false + + queuedVersionIds.add(versionId) + if (resolvedProjectId) { + queuedProjectVersions.set(resolvedProjectId, versionId) + } + queuedInstalls.push({ versionId, projectId: resolvedProjectId }) + return true + } + + const announceDependency = async (projectId, resolvedVersion) => { + if (!onDepInstalling || !projectId) return + if (announcedProjects.has(projectId)) return + + const depProject = await get_project(projectId, 'bypass').catch(() => null) + if (!depProject) return + + storeProjectName(depProject) + onDepInstalling(depProject, resolvedVersion ?? undefined) + announcedProjects.add(projectId) + } + + const resolveDependency = async (dep) => { + let depVersion = null + let depProjectId = dep.project_id ?? null + if (dep.version_id) { - if (dep.project_id && (await check_installed(profile.path, dep.project_id))) continue - if (dep.project_id && onDepInstalling) { - const depProject = await get_project(dep.project_id, 'bypass').catch(() => null) - if (depProject) onDepInstalling(depProject) + depVersion = await get_version(dep.version_id, 'bypass').catch(() => null) + if (!depVersion) return null + + depProjectId = depProjectId ?? depVersion.project_id ?? null + if (depProjectId && !projectNames.has(depProjectId)) { + const p = await get_project(depProjectId, 'bypass').catch(() => null) + storeProjectName(p) } - await add_project_from_version(profile.path, dep.version_id) - } else { - if (dep.project_id && (await check_installed(profile.path, dep.project_id))) continue + } else if (dep.project_id) { + const depProject = await get_project(dep.project_id, 'bypass').catch(() => null) + if (!depProject) return null - const depProject = await get_project(dep.project_id, 'bypass') - if (onDepInstalling) onDepInstalling(depProject) + storeProjectName(depProject) - const depVersions = (await get_version_many(depProject.versions, 'bypass')).sort( - (a, b) => dayjs(b.date_published) - dayjs(a.date_published), + const depVersions = await get_version_many(depProject.versions, 'bypass').catch(() => []) + depVersion = findPreferredVersion( + depVersions.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published)), + dep, + profile, ) + if (!depVersion) return null + + depProjectId = dep.project_id + } else { + return null + } + + return { depVersion, depProjectId } + } + + const collectDependenciesForVersion = async (inputVersion) => { + if (!inputVersion?.id || visitedVersions.has(inputVersion.id)) return + visitedVersions.add(inputVersion.id) - const latest = findPreferredVersion(depVersions, dep, profile) - if (latest) { - await add_project_from_version(profile.path, latest.id) + if (inputVersion.project_id && !projectNames.has(inputVersion.project_id)) { + const p = await get_project(inputVersion.project_id, 'bypass').catch(() => null) + storeProjectName(p) + } + + for (const dep of inputVersion.dependencies ?? []) { + if (dep.dependency_type !== 'required') continue + if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue + + const resolved = await resolveDependency(dep, inputVersion) + if (!resolved) continue + + const { depVersion, depProjectId } = resolved + const queued = await queueInstall(depProjectId, depVersion) + if (queued && depProjectId) { + await announceDependency(depProjectId, depVersion) } + + await collectDependenciesForVersion(depVersion) } } + + await collectDependenciesForVersion(version) + + if (queuedInstalls.length === 0) return + + const batchSize = 8 + for (let i = 0; i < queuedInstalls.length; i += batchSize) { + const batch = queuedInstalls.slice(i, i + batchSize) + await Promise.all( + batch.map(async ({ versionId }) => { + await add_project_from_version(profile.path, versionId) + }), + ) + } } export const getServerAddress = (javaServer) => {