"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) { const [checkpoint, setCheckpoint] = useState(null) const current = useRef(null) const [hydrated, setHydrated] = useState(false), [busy, setBusy] = useState(false) const [error, setError] = useState(null), [storageError, setStorageError] = useState(null) const alive = useRef(true), lock = useRef(false), controller = useRef(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 } }