feat: content upload session impl (#6992)

* feat: content upload session impl

* fix: fixes

* fix: prepr
This commit is contained in:
Calum H.
2026-08-14 13:43:09 +00:00
committed by GitHub
parent 17f13097f2
commit 4ca48e4589
11 changed files with 285 additions and 216 deletions
@@ -7,6 +7,10 @@ export type UploadSessionFile = {
filename: string
}
function getUploadSessionPath(scope: Kyros.UploadSessions.v1.Scope, worldId: string) {
return `/worlds/${worldId}/${scope}/upload-session`
}
export class KyrosUploadSessionsV1Module extends AbstractModule {
public getModuleID(): string {
return 'kyros_upload_sessions_v1'
@@ -17,7 +21,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
worldId: string,
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
`/worlds/${worldId}/files/upload-session`,
getUploadSessionPath(scope, worldId),
{
api: '',
version: 'v1',
@@ -32,7 +36,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
worldId: string,
): Promise<Kyros.UploadSessions.v1.GetUploadSessionResponse> {
return this.client.request<Kyros.UploadSessions.v1.GetUploadSessionResponse>(
`/worlds/${worldId}/files/upload-session`,
getUploadSessionPath(scope, worldId),
{
api: '',
version: 'v1',
@@ -58,7 +62,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
}
return this.client.upload<Kyros.UploadSessions.v1.UploadSessionResponse>(
`/worlds/${worldId}/files/upload-session/${uploadId}/files`,
`${getUploadSessionPath(scope, worldId)}/${uploadId}/files`,
{
api: '',
version: 'v1',
@@ -76,7 +80,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
uploadId: string,
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
`/worlds/${worldId}/files/upload-session/${uploadId}/finalize`,
`${getUploadSessionPath(scope, worldId)}/${uploadId}/finalize`,
{
api: '',
version: 'v1',
@@ -92,7 +96,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
uploadId: string,
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
`/worlds/${worldId}/files/upload-session/${uploadId}`,
`${getUploadSessionPath(scope, worldId)}/${uploadId}`,
{
api: '',
version: 'v1',
@@ -68,6 +68,7 @@ useQuery({
from_modpack: false,
}),
enabled: computed(() => !!worldId.value),
staleTime: 30_000,
})
const serverSettingsTabComponentMap = {
@@ -199,7 +199,6 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
}
: current,
)
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
}
function handleWorldContentBaseUpdate(
@@ -31,6 +31,11 @@ export interface BrowseInstallTarget {
loader?: string | null
}
export interface BrowseResolvedInstallContent {
projectId: string
versionId: string
}
/**
* Minimal project shape needed by shared install resolution.
*/
@@ -59,6 +64,7 @@ export interface BrowseInstallPlan<TProject extends BrowseInstallProject = Brows
contentType: BrowseInstallContentType
preferences: BrowseInstallPreferences
source: BrowseInstallPlanSource
resolvedContent?: BrowseResolvedInstallContent[]
}
/**
@@ -335,6 +341,15 @@ export interface FlushStoredServerAddonInstallQueueOptions<TProject extends Brow
onQueueChange?: (plans: Map<string, BrowseInstallPlan<TProject>>) => void
}
export interface ResolveServerAddonInstallPlansOptions<TProject extends BrowseInstallProject> {
plans: readonly BrowseInstallPlan<TProject>[]
existingProjectIds: Iterable<string>
resolvePlan: (
plan: BrowseInstallPlan<TProject>,
existingProjectIds: string[],
) => Promise<BrowseResolvedInstallContent[]>
}
/**
* Result of a queue flush. Failed plans are also written back to the queue.
*/
@@ -646,6 +661,62 @@ export function getStoredServerAddonInstallQueue<
return addonPlans
}
export function getServerAddonInstallPlanProjectIds<TProject extends BrowseInstallProject>(
plans: Iterable<BrowseInstallPlan<TProject>>,
) {
const projectIds = new Set<string>()
for (const plan of plans) {
projectIds.add(plan.projectId)
for (const item of plan.resolvedContent ?? []) {
projectIds.add(item.projectId)
}
}
return projectIds
}
export async function resolveServerAddonInstallPlans<TProject extends BrowseInstallProject>({
plans,
existingProjectIds,
resolvePlan,
}: ResolveServerAddonInstallPlansOptions<TProject>) {
const installedProjectIds = new Set(existingProjectIds)
const resolutionExistingProjectIds = Array.from(installedProjectIds)
const resolvedPlans = await Promise.all(
plans.map(
async (plan) =>
plan.resolvedContent ?? (await resolvePlan(plan, resolutionExistingProjectIds)),
),
)
const explicitProjectIds = new Set(plans.map((plan) => plan.projectId))
const addons = new Map<string, { project_id: string; version_id: string }>()
for (const plan of plans) {
if (installedProjectIds.has(plan.projectId)) continue
addons.set(plan.projectId, {
project_id: plan.projectId,
version_id: plan.versionId,
})
}
for (const content of resolvedPlans) {
for (const item of content) {
if (
installedProjectIds.has(item.projectId) ||
explicitProjectIds.has(item.projectId) ||
addons.has(item.projectId)
) {
continue
}
addons.set(item.projectId, {
project_id: item.projectId,
version_id: item.versionId,
})
}
}
return Array.from(addons.values())
}
export async function flushStoredServerAddonInstallQueue<TProject extends BrowseInstallProject>({
serverId,
worldId,
@@ -869,7 +940,22 @@ function isStoredBrowseInstallPlan(
typeof record.versionId === 'string' &&
isStoredBrowseInstallContentType(record.contentType) &&
isStoredBrowseInstallPreferences(record.preferences) &&
(record.source === 'filtered' || record.source === 'target')
(record.source === 'filtered' || record.source === 'target') &&
isOptionalStoredResolvedContent(record.resolvedContent)
)
}
function isOptionalStoredResolvedContent(value: unknown) {
return (
value === undefined ||
(Array.isArray(value) &&
value.every(
(item) =>
!!item &&
typeof item === 'object' &&
typeof (item as Record<string, unknown>).projectId === 'string' &&
typeof (item as Record<string, unknown>).versionId === 'string',
))
)
}
@@ -245,6 +245,7 @@ const addonsQuery = useQuery({
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
enabled: computed(() => worldId.value !== null),
staleTime: 30_000,
})
const modpack = computed(() => addonsQuery.data.value?.modpack ?? null)
@@ -411,6 +412,9 @@ async function uploadLocalModpackWithSoftOverride() {
provideInstallationSettings({
closeSettings: serverSettings.closeModal,
afterSave: async () => {
serverSettings.closeModal?.()
},
onGameVersionHover: handleGameVersionHover,
loading: computed(() => !server.value || addonsQuery.isLoading.value),
installationInfo: computed(() => {
@@ -32,6 +32,7 @@ import {
flushStoredServerAddonInstallQueue,
getStoredServerAddonInstallQueue,
getTargetInstallPreferences,
resolveServerAddonInstallPlans,
} from '../../../shared/browse-tab/composables/install-logic'
import ManagedContentModal from '../../../shared/content-tab/components/managed-content-modal/index.vue'
import ConfirmModpackUpdateModal from '../../../shared/content-tab/components/modals/ConfirmModpackUpdateModal.vue'
@@ -157,7 +158,7 @@ const contentQuery = useQuery({
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
enabled: computed(() => worldId.value !== null),
staleTime: 0,
staleTime: 30_000,
})
const isModpackContentModalOpen = ref(false)
@@ -168,7 +169,7 @@ const modpackContentQuery = useQuery({
from_modpack: true,
}),
enabled: computed(() => worldId.value !== null && !!contentQuery.data.value?.modpack),
staleTime: 0,
staleTime: 30_000,
})
const setupActionDisabled = computed(() => !canSetup.value || busyReasons.value.length > 0)
@@ -395,37 +396,32 @@ function toResolvePreferences(
}
async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) {
const existingProjectIds = getInstalledProjectIds()
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
for (const plan of plans) {
const target = getTargetInstallPreferences(
{
gameVersion: server.value?.mc_version,
loader: server.value?.loader,
},
plan.contentType,
)
const resolved = await client.labrinth.content_v3.resolve({
project_id: plan.projectId,
version_id: plan.versionId,
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
selected: toResolvePreferences(plan.preferences),
target: toResolvePreferences(target),
existing_project_ids: Array.from(existingProjectIds),
})
for (const item of [resolved.primary, ...resolved.dependencies]) {
if (existingProjectIds.has(item.project_id)) continue
existingProjectIds.add(item.project_id)
resolvedAddons.push({
project_id: item.project_id,
version_id: item.version_id,
return await resolveServerAddonInstallPlans({
plans,
existingProjectIds: getInstalledProjectIds(),
resolvePlan: async (plan, existingProjectIds) => {
const target = getTargetInstallPreferences(
{
gameVersion: server.value?.mc_version,
loader: server.value?.loader,
},
plan.contentType,
)
const resolved = await client.labrinth.content_v3.resolve({
project_id: plan.projectId,
version_id: plan.versionId,
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
selected: toResolvePreferences(plan.preferences),
target: toResolvePreferences(target),
existing_project_ids: existingProjectIds,
})
}
}
return resolvedAddons
return [resolved.primary, ...resolved.dependencies].map((item) => ({
projectId: item.project_id,
versionId: item.version_id,
}))
},
})
}
function addonMatchesPendingInstall(
@@ -1124,8 +1124,13 @@ const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) =>
)
}
let newModInvalidateTimer: ReturnType<typeof setTimeout> | null = null
const handleNewMod = () => {
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
if (newModInvalidateTimer) clearTimeout(newModInvalidateTimer)
newModInvalidateTimer = setTimeout(() => {
newModInvalidateTimer = null
void queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
}, 500)
}
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
@@ -1520,6 +1525,10 @@ function initializeServer() {
const cleanup = () => {
isMounted.value = false
if (newModInvalidateTimer) {
clearTimeout(newModInvalidateTimer)
newModInvalidateTimer = null
}
saveWsStateToCache()