68 lines
4.7 KiB
TypeScript
68 lines
4.7 KiB
TypeScript
"use client"
|
|
import { useEffect, useRef, useState } from "react"
|
|
import type { CreateBackupRequest } from "@mmapp/contracts/backups"
|
|
import { ApiClientError } from "@/shared/api/http-client"
|
|
import { createBackupsAsync } from "@/shared/api/backups"
|
|
import { pollBackupJob, backupJobOutcome, backupJobResultError } from "@/lib/backup-job-poll"
|
|
import { backupJobStorageKey, createBackupJobCheckpoint, readBackupJobCheckpoint, type BackupJobCheckpoint } from "@/lib/backup-job-checkpoint"
|
|
import { userApiError } from "@/shared/api/users"
|
|
import { toast } from "sonner"
|
|
|
|
export function useBackupJob(backendUrl: string, live: boolean, canRead: boolean, canCreate: boolean, onDone: () => Promise<void>) {
|
|
const [checkpoint, setCheckpoint] = useState<BackupJobCheckpoint | null>(null)
|
|
const current = useRef<BackupJobCheckpoint | null>(null)
|
|
const [hydrated, setHydrated] = useState(false), [busy, setBusy] = useState(false)
|
|
const [error, setError] = useState<string | null>(null), [storageError, setStorageError] = useState<string | null>(null)
|
|
const alive = useRef(true), lock = useRef(false), controller = useRef<AbortController | null>(null)
|
|
const key = backupJobStorageKey(backendUrl)
|
|
function persist(value: BackupJobCheckpoint | null, required = false) {
|
|
try { if (value) sessionStorage.setItem(key, JSON.stringify(value)); else sessionStorage.removeItem(key); setStorageError(null) }
|
|
catch { if (required) throw new Error("Не удалось сохранить задачу в этой вкладке. Запрос не отправлен"); setStorageError("Не удалось обновить запись задачи. Держите вкладку открытой до подтверждения результата") }
|
|
current.current = value; setCheckpoint(value)
|
|
}
|
|
useEffect(() => {
|
|
alive.current = true
|
|
queueMicrotask(() => {
|
|
if (!alive.current) return
|
|
try { const saved = live ? readBackupJobCheckpoint(sessionStorage.getItem(key)) : null; current.current = saved; setCheckpoint(saved) }
|
|
catch { setStorageError("Не удалось восстановить задачу. Новое создание заблокировано, чтобы не отправить повтор") }
|
|
setHydrated(true)
|
|
})
|
|
return () => { alive.current = false; controller.current?.abort() }
|
|
}, [key, live])
|
|
async function run(request?: CreateBackupRequest) {
|
|
if (!live || !hydrated || !canRead || lock.current || !alive.current) return
|
|
if (!current.current && (!canCreate || storageError || !request)) return
|
|
lock.current = true; setBusy(true); setError(null)
|
|
const abort = new AbortController(); controller.current = abort
|
|
const active = () => alive.current && !abort.signal.aborted
|
|
let saved = current.current, creating = false
|
|
try {
|
|
if (!saved) {
|
|
saved = createBackupJobCheckpoint(request!, crypto.randomUUID()); persist(saved, true); creating = true
|
|
await createBackupsAsync(backendUrl, { ...saved.request, serverIds: saved.request.serverIds.map(String) }, { signal: abort.signal })
|
|
if (!active()) return
|
|
saved = { ...saved, accepted: true }; persist(saved)
|
|
}
|
|
// A restored/unknown request is only observed. In-memory backend records cannot safely justify another POST after 404.
|
|
const target = saved.request
|
|
const result = await pollBackupJob(backendUrl, target.requestId, target, { signal: abort.signal, onJob: job => { if (active()) persist({ request: target, accepted: true, job }) } })
|
|
if (!active()) return
|
|
const outcome = backupJobOutcome(result)
|
|
if (outcome.fullSuccess) toast.success("API подтвердил создание всех резервных копий", { id: "backup-job-" + result.id })
|
|
else setError(backupJobResultError(result))
|
|
await onDone()
|
|
} catch (reason) {
|
|
if (!active()) return
|
|
if (creating && !saved?.accepted && reason instanceof ApiClientError && [400, 403, 404, 409].includes(reason.status)) persist(null)
|
|
const message = reason instanceof ApiClientError && reason.status === 404 && current.current ? "Задача недоступна на сервере. После рестарта сведения могли быть потеряны; проверьте историю и устройства" : userApiError(reason)
|
|
setError(message + (current.current ? ". Повторяется только чтение этой задачи; новое создание не запускается" : ""))
|
|
} finally { lock.current = false; if (active()) setBusy(false) }
|
|
}
|
|
function reset() {
|
|
if (lock.current || !checkpoint?.job || !["done", "failed"].includes(checkpoint.job.status)) return false
|
|
try { persist(null, true); setError(null); return true } catch (reason) { setError(userApiError(reason)); return false }
|
|
}
|
|
return { checkpoint, hydrated, busy, error, storageError, run, reset }
|
|
}
|