Files
MikrotikManager/lib/use-certificate-issue.ts
T

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