Files
MikrotikManager/lib/use-backup-job.ts

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