"use client" import { useEffect, useRef, useState } from "react" import type { CertificateIssueRequest } from "@mmapp/contracts/certificates" import { ApiClientError } from "@/shared/api/http-client" import { createCertificateIssueJob, getCertificateIssueJob } from "@/shared/api/certificates" import { pollCertificateIssue } from "@/lib/certificate-issue-poll" import { certificateIssueStorageKey, createCertificateIssueCheckpoint, readCertificateIssueCheckpoint, type CertificateIssueCheckpoint } from "@/lib/certificate-issue-checkpoint" import { toast } from "sonner" export function useCertificateIssue(backendUrl: string, canRead: boolean, canWrite: boolean, onDone: () => Promise, live = true) { const [checkpoint, setCheckpoint] = useState(null) const current = useRef(null) const [hydrated, setHydrated] = useState(false) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [storageError, setStorageError] = useState(null) const alive = useRef(true), lock = useRef(false) const controller = useRef(null) const key = certificateIssueStorageKey(backendUrl) function persist(value: CertificateIssueCheckpoint | null, requireStorage = false) { try { if (value) sessionStorage.setItem(key, JSON.stringify(value)); else sessionStorage.removeItem(key) setStorageError(null) } catch { if (requireStorage) throw new Error("Не удалось сохранить задачу в этой вкладке.") setStorageError("Не удалось обновить сохранённую задачу. Держите вкладку открытой до подтверждения результата.") } current.current = value setCheckpoint(value) } useEffect(() => { alive.current = true queueMicrotask(() => { if (!alive.current) return try { const saved = live ? readCertificateIssueCheckpoint(sessionStorage.getItem(key)) : null current.current = saved; setCheckpoint(saved) } catch { setStorageError("Не удалось восстановить сохранённую задачу. Новый выпуск заблокирован, чтобы не создать повторную задачу.") } setHydrated(true) }) const cancel = () => { alive.current = false; controller.current?.abort() } return cancel }, [key, live]) async function run(request?: CertificateIssueRequest) { if (!hydrated || !canRead || lock.current || !alive.current) return if (!current.current && (!canWrite || 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 = createCertificateIssueCheckpoint(request!, crypto.randomUUID()) persist(saved, true) creating = true } else if (!saved.accepted) { try { const job = await getCertificateIssueJob(backendUrl, saved.request.requestId, { signal: abort.signal }) if (!active()) return // pollCertificateIssue проверит цель до записи наблюдения. if (job.id !== saved.request.requestId) throw new Error("Получена другая задача") } catch (reason) { if (!(reason instanceof ApiClientError) || reason.status !== 404) throw reason creating = true } } if (creating) { if (!canWrite) throw new Error("Нет права mm:certificates:write для повторного подтверждения запроса") await createCertificateIssueJob(backendUrl, saved.request, { signal: abort.signal }) if (!active()) return saved = { ...saved, accepted: true }; persist(saved) } const target = saved.request const result = await pollCertificateIssue(backendUrl, target.requestId, target, { signal: abort.signal, onJob: job => { if (active()) persist({ request: target, accepted: true, job }) } }) if (!active()) return if (result.status === "failed") setError((result.error ?? "Задача завершилась ошибкой") + ". Частичное выполнение возможно; проверьте состояние устройства перед новым выпуском.") else { toast.success("API подтвердил завершение задачи выпуска", { id: "certificate-issue-" + result.id }) await onDone() } } catch (reason) { if (!active()) return if (creating && !saved?.accepted && reason instanceof ApiClientError && [400, 403, 404].includes(reason.status)) persist(null) setError((reason instanceof Error ? reason.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(reason instanceof Error ? reason.message : "Не удалось очистить запись"); return false } } return { checkpoint, hydrated, busy, error, storageError, run, reset } }