import { bindingDiffKey, userInitials, type AppUser, type AppUserForm, type InterfaceBinding } from "@/lib/users" import { ApiClientError } from "@/shared/api/http-client" export interface UserSaveCheckpoint { user: AppUser | null; profileSignature?: string; bindingsNeedRead?: boolean; createUncertain?: boolean } export interface UserSaveApi { create: (form: AppUserForm) => Promise update: (id: string, form: AppUserForm) => Promise read: (id: string) => Promise removeBinding: (userId: string, bindingId: string) => Promise addBinding: (userId: string, binding: InterfaceBinding) => Promise } /** Подтверждённые шаги нескольких HTTP запросов, а не общая транзакция. */ export async function saveUserSteps(form: AppUserForm, checkpoint: UserSaveCheckpoint, api: UserSaveApi, current: () => boolean, changed: (state: UserSaveCheckpoint) => void): Promise { if (!current()) return null if (checkpoint.createUncertain) throw new Error("Результат создания неизвестен. Проверьте список пользователей перед новой попыткой; повторное создание заблокировано.") const { bindings: desired, ...profile } = form const signature = JSON.stringify(profile) if (checkpoint.bindingsNeedRead && checkpoint.user) { const user = await api.read(checkpoint.user.id) if (!current()) return null checkpoint.user = user; checkpoint.bindingsNeedRead = false; checkpoint.profileSignature = undefined; changed(checkpoint) } if (!checkpoint.user) { try { const user = await api.create(form) if (!current()) return null checkpoint.user = user; checkpoint.profileSignature = signature; changed(checkpoint) } catch (error) { if (current() && !(error instanceof ApiClientError && error.status >= 400 && error.status < 500)) { checkpoint.createUncertain = true; changed(checkpoint) } throw error } } else if (checkpoint.profileSignature !== signature) { const user = await api.update(checkpoint.user.id, form) if (!current()) return null checkpoint.user = user; checkpoint.profileSignature = signature; changed(checkpoint) } if (!checkpoint.user || !current()) return null try { const desiredKeys = new Set(desired.map(bindingDiffKey)) for (const binding of [...checkpoint.user.bindings]) { if (desiredKeys.has(bindingDiffKey(binding))) continue await api.removeBinding(checkpoint.user.id, binding.id) if (!current()) return null checkpoint.user = { ...checkpoint.user, bindings: checkpoint.user.bindings.filter(b => b.id !== binding.id) }; changed(checkpoint) } const knownKeys = new Set(checkpoint.user.bindings.map(bindingDiffKey)) for (const binding of desired) { if (knownKeys.has(bindingDiffKey(binding))) continue const added = await api.addBinding(checkpoint.user.id, binding) if (!current()) return null checkpoint.user = { ...checkpoint.user, bindings: [...checkpoint.user.bindings, added] }; knownKeys.add(bindingDiffKey(added)); changed(checkpoint) } } catch (error) { if (current()) { checkpoint.bindingsNeedRead = true; changed(checkpoint) } throw error } return checkpoint.user } export function userProfileRequest(form: AppUserForm) { return { name: form.name.trim(), login: form.login.trim(), email: form.email.trim(), role: form.role, active: form.active, avatar: userInitials(form.name), sections: form.sections, servers: form.servers } }