Files
ftb-project-management/apps/web/stores/useXiaobaoWarningReadStore.ts
2026-07-03 09:43:36 +08:00

99 lines
3.4 KiB
TypeScript

'use client';
import { create } from 'zustand';
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import {
markXiaobaoWarningRead,
type XiaobaoWarningReadState,
} from '@/lib/xiaobao-warning-view';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
interface XiaobaoWarningReadStoreState {
readStates: XiaobaoWarningReadState[];
readStateLoaded: boolean;
error?: string;
fetchReadStates: () => Promise<void>;
markRiskRead: (userId: string, risk: XiaobaoVersionRisk, readAt?: string) => Promise<void>;
}
let readStateSaveQueue: Promise<void> = Promise.resolve();
let lastReadStatesFetchAt = 0;
async function loadReadStates(): Promise<XiaobaoWarningReadState[] | null> {
try {
const rows = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
return normalizeReadStates(rows);
} catch {}
return null;
}
function normalizeReadStates(rows: unknown): XiaobaoWarningReadState[] {
if (!Array.isArray(rows)) return [];
return rows.filter(isReadState);
}
function isReadState(row: unknown): row is XiaobaoWarningReadState {
if (!row || typeof row !== 'object') return false;
const item = row as Partial<XiaobaoWarningReadState>;
return (
typeof item.userId === 'string' &&
typeof item.versionId === 'string' &&
typeof item.signature === 'string' &&
typeof item.readAt === 'string'
);
}
function mergeReadStates(
remoteRows: XiaobaoWarningReadState[] = [],
localRows: XiaobaoWarningReadState[] = [],
): XiaobaoWarningReadState[] {
const byKey = new Map<string, XiaobaoWarningReadState>();
for (const row of [...remoteRows, ...localRows]) {
const key = `${row.userId}::${row.versionId}`;
const existing = byKey.get(key);
if (!existing || row.readAt.localeCompare(existing.readAt) >= 0) {
byKey.set(key, row);
}
}
return Array.from(byKey.values()).sort((a, b) => b.readAt.localeCompare(a.readAt));
}
export const useXiaobaoWarningReadStore = create<XiaobaoWarningReadStoreState>((set, get) => ({
readStates: [],
readStateLoaded: false,
error: undefined,
fetchReadStates: async () => {
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < SERVER_DATA_CACHE_MS) return;
const rows = await loadReadStates();
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < SERVER_DATA_CACHE_MS) return;
lastReadStatesFetchAt = Date.now();
set({
readStates: rows ? mergeReadStates(rows, get().readStates) : get().readStates,
readStateLoaded: rows !== null,
error: rows === null ? '小宝预警查看状态加载失败' : undefined,
});
},
markRiskRead: async (userId, risk, readAt = new Date().toISOString()) => {
if (!userId) return;
const optimistic = markXiaobaoWarningRead(get().readStates, userId, risk, readAt);
set({ readStates: optimistic, error: undefined });
const task = readStateSaveQueue.then(async () => {
const remote = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views', { force: true });
const merged = mergeReadStates(normalizeReadStates(remote), get().readStates);
set({ readStates: merged, readStateLoaded: true, error: undefined });
await saveServerData('xiaobao-warning-views', merged);
});
readStateSaveQueue = task.catch(() => undefined);
try {
await task;
} catch (error) {
set({ error: '小宝预警查看状态保存失败' });
throw error;
}
},
}));