102 lines
5.4 KiB
TypeScript
102 lines
5.4 KiB
TypeScript
"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<void>, live = true) {
|
|
const [checkpoint, setCheckpoint] = useState<CertificateIssueCheckpoint | null>(null)
|
|
const current = useRef<CertificateIssueCheckpoint | null>(null)
|
|
const [hydrated, setHydrated] = useState(false)
|
|
const [busy, setBusy] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [storageError, setStorageError] = useState<string | null>(null)
|
|
const alive = useRef(true), lock = useRef(false)
|
|
const controller = useRef<AbortController | null>(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 }
|
|
}
|