import { api, ApiRequestError } from './api'; export type ServerDataKey = | 'products-overview' | 'requirements' | 'version-plans' | 'dev-tasks' | 'test-cases' | 'bugs' | 'members' | 'task-categories' | 'task-worklogs' | 'work-activities' | 'xiaobao-risk-insights' | 'xiaobao-risk-snapshots' | 'xiaobao-warning-views' | 'overtime'; interface ServerDataResponse { key: ServerDataKey; value: T | null; version: string | null; } type ServerDataConflictBody = { code: 'APP_DATA_CONFLICT'; key: ServerDataKey; currentValue: T | null; currentVersion: string | null; }; type ServerDataWriteFrozenBody = { code: 'APP_DATA_WRITE_FROZEN'; key: ServerDataKey; state: 'write_frozen' | 'read_only_archive'; replacement: string; note?: string; }; const serverDataVersions = new Map(); const serverDataCache = new Map(); const serverDataLoadPromises = new Map>(); const serverDataSaveQueues = new Map>(); export const SERVER_DATA_CACHE_MS = 30_000; export const SERVER_DATA_SAVE_ERROR_EVENT = 'ftb:server-data-save-error'; export type ServerDataSaveErrorDetail = { key: ServerDataKey; message: string; }; type LoadServerDataOptions = { force?: boolean; maxAgeMs?: number; }; export class ServerDataConflictError extends Error { constructor( public readonly key: ServerDataKey, public readonly currentValue: T | null, public readonly currentVersion: string | null, ) { super('Server data was changed by another user. Reload before saving again.'); this.name = 'ServerDataConflictError'; } } export class ServerDataWriteFrozenError extends Error { constructor( public readonly key: ServerDataKey, public readonly state: 'write_frozen' | 'read_only_archive', public readonly replacement: string, public readonly note?: string, ) { super(`AppData 写入已冻结,请改用 ${replacement}`); this.name = 'ServerDataWriteFrozenError'; } } function getServerDataSaveErrorMessage(error: unknown) { if (error instanceof ServerDataConflictError) return '数据已被其他用户更新,请刷新后重试'; if (error instanceof ServerDataWriteFrozenError) return error.message; if (error instanceof Error && error.message) return error.message; return '保存失败,请检查网络或 API 服务'; } function notifyServerDataSaveFailure(key: ServerDataKey, error: unknown) { if (typeof window === 'undefined') return; window.dispatchEvent( new CustomEvent(SERVER_DATA_SAVE_ERROR_EVENT, { detail: { key, message: getServerDataSaveErrorMessage(error), }, }), ); } export async function loadServerData( key: ServerDataKey, options: LoadServerDataOptions = {}, ): Promise { const maxAgeMs = options.maxAgeMs ?? SERVER_DATA_CACHE_MS; const cached = serverDataCache.get(key); if (!options.force && cached && maxAgeMs > 0 && Date.now() - cached.loadedAt <= maxAgeMs) { return cached.value as T | null; } const inFlight = serverDataLoadPromises.get(key); if (inFlight) return inFlight as Promise; const promise = api.get>(`/data/${key}`) .then((res) => { const version = res.version ?? null; serverDataVersions.set(key, version); serverDataCache.set(key, { value: res.value, version, loadedAt: Date.now(), }); return res.value; }) .finally(() => { serverDataLoadPromises.delete(key); }); serverDataLoadPromises.set(key, promise as Promise); return promise; } export function saveServerData(key: ServerDataKey, value: T): Promise { const previousSave = serverDataSaveQueues.get(key) ?? Promise.resolve(); const save = previousSave.then(() => putServerData(key, value)); const queueTail = save.catch(() => undefined); serverDataSaveQueues.set(key, queueTail); void queueTail.then(() => { if (serverDataSaveQueues.get(key) === queueTail) { serverDataSaveQueues.delete(key); } }); return save; } async function putServerData(key: ServerDataKey, value: T): Promise { const payload: { value: T; version?: string | null } = { value }; if (serverDataVersions.has(key)) { payload.version = serverDataVersions.get(key) ?? null; } try { const res = await api.put>(`/data/${key}`, payload); const version = res.version ?? null; serverDataVersions.set(key, version); serverDataCache.set(key, { value, version, loadedAt: Date.now(), }); } catch (error) { if ( error instanceof ApiRequestError && error.status === 409 && isServerDataConflictBody(error.body, key) ) { const conflict = new ServerDataConflictError( error.body.key, error.body.currentValue, error.body.currentVersion, ); notifyServerDataSaveFailure(key, conflict); throw conflict; } if ( error instanceof ApiRequestError && error.status === 409 && isServerDataWriteFrozenBody(error.body, key) ) { const frozen = new ServerDataWriteFrozenError( error.body.key, error.body.state, error.body.replacement, error.body.note, ); notifyServerDataSaveFailure(key, frozen); throw frozen; } notifyServerDataSaveFailure(key, error); throw error; } } function isServerDataConflictBody( body: unknown, key: ServerDataKey, ): body is ServerDataConflictBody { return ( typeof body === 'object' && body !== null && (body as { code?: unknown }).code === 'APP_DATA_CONFLICT' && (body as { key?: unknown }).key === key && ('currentVersion' in body ? typeof (body as { currentVersion?: unknown }).currentVersion === 'string' || (body as { currentVersion?: unknown }).currentVersion === null : true) ); } function isServerDataWriteFrozenBody( body: unknown, key: ServerDataKey, ): body is ServerDataWriteFrozenBody { return ( typeof body === 'object' && body !== null && (body as { code?: unknown }).code === 'APP_DATA_WRITE_FROZEN' && (body as { key?: unknown }).key === key && ((body as { state?: unknown }).state === 'write_frozen' || (body as { state?: unknown }).state === 'read_only_archive') && typeof (body as { replacement?: unknown }).replacement === 'string' ); }