fix(plugin): serialize project refresh mutations

This commit is contained in:
2026-08-27 21:42:33 +08:00
parent 405b9f64fd
commit e3e34d3803
3 changed files with 283 additions and 27 deletions

View File

@@ -44,21 +44,53 @@ export function createCodingPluginsStore(overrides: Partial<Dependencies> = {}):
removeProject: removeDataServiceProject, ...overrides,
};
const flights = new Map<string, Promise<void>>();
const pendingCounts = new Map<string, number>();
const projectEpochs = new Map<string, number>();
let loadGeneration = 0;
const operation = (key: string, run: () => Promise<void>): Promise<void> => {
let latestLoadProjectId: string | null = null;
const projectEpoch = (projectId: string): number => projectEpochs.get(projectId) ?? 0;
const advanceProjectEpoch = (projectId: string): void => {
projectEpochs.set(projectId, projectEpoch(projectId) + 1);
};
const isCurrentProject = (projectId: string | null): projectId is string => (
projectId !== null
&& store.getState().projectId === projectId
&& (latestLoadProjectId === null || latestLoadProjectId === projectId)
);
const operation = (
key: string,
run: () => Promise<void>,
pendingKey = key,
): Promise<void> => {
const existing = flights.get(key); if (existing) return existing;
const pendingCount = (pendingCounts.get(pendingKey) ?? 0) + 1;
pendingCounts.set(pendingKey, pendingCount);
let flight: Promise<void>;
flight = run().finally(() => {
flights.delete(key);
const pending = { ...store.getState().pending }; delete pending[key]; store.setState({ pending });
if (flights.get(key) === flight) flights.delete(key);
const remaining = (pendingCounts.get(pendingKey) ?? 1) - 1;
if (remaining > 0) {
pendingCounts.set(pendingKey, remaining);
} else {
pendingCounts.delete(pendingKey);
const pending = { ...store.getState().pending };
delete pending[pendingKey];
store.setState({ pending });
}
});
flights.set(key, flight); store.setState((state) => ({ pending: { ...state.pending, [key]: true } }));
flights.set(key, flight);
if (pendingCount === 1) {
store.setState((state) => ({ pending: { ...state.pending, [pendingKey]: true } }));
}
return flight;
};
const store = createStore<CodingPluginsState>((set, get) => ({
projectId: null, projection: null, dataService: null, loadState: 'idle', error: null, pending: {},
load(projectId) {
const key = `load:${projectId}`;
latestLoadProjectId = projectId;
const epoch = projectEpoch(projectId);
const pendingKey = `load:${projectId}`;
const key = `${pendingKey}:${epoch}`;
const existing = flights.get(key);
if (existing) return existing;
const generation = ++loadGeneration;
@@ -72,78 +104,82 @@ export function createCodingPluginsStore(overrides: Partial<Dependencies> = {}):
const result = await deps.inspectDataService();
if (result.success && result.data) dataService = result.data;
}
if (generation === loadGeneration) {
if (generation === loadGeneration && epoch === projectEpoch(projectId)) {
set({ projectId, projection, dataService, loadState: 'ready', error: null });
}
} catch (error) {
if (generation === loadGeneration) {
if (generation === loadGeneration && epoch === projectEpoch(projectId)) {
set({ loadState: 'error', error: error instanceof Error ? error.message : String(error) });
}
throw error;
}
});
}, pendingKey);
},
setEnabled(projectId, pluginId, enabled) {
const generation = loadGeneration;
return operation(`enabled:${projectId}:${pluginId}`, async () => {
const projection = await deps.setEnabled(projectId, pluginId, enabled);
if (generation !== loadGeneration || get().projectId !== projectId) return;
if (!isCurrentProject(projectId)) return;
let dataService = get().dataService;
const item = projection.items.find(({ id }) => id === pluginId);
if (!enabled) {
dataService = null;
} else if (item?.settingsSurface === 'data-service' && item.backend.status === 'ready') {
const inspected = await deps.inspectDataService();
if (generation !== loadGeneration || get().projectId !== projectId) return;
if (!isCurrentProject(projectId)) return;
if (inspected.success && inspected.data) dataService = inspected.data;
}
advanceProjectEpoch(projectId);
set({
projectId, projection, dataService, error: null,
projectId, projection, dataService, loadState: 'ready', error: null,
});
});
},
configure(collections) {
const projectId = get().projectId;
const generation = loadGeneration;
return operation('data-service:configure', async () => {
const result = await deps.configureDataService(collections);
if (!result.success || !result.data) throw new Error(result.error || '开发数据空间创建失败');
if (generation !== loadGeneration || get().projectId !== projectId) return;
set({ dataService: result.data, error: null });
if (projectId) await get().load(projectId);
if (!isCurrentProject(projectId)) return;
advanceProjectEpoch(projectId);
set({ dataService: result.data, loadState: 'ready', error: null });
await get().load(projectId);
});
},
reset() {
const projectId = get().projectId;
const generation = loadGeneration;
return operation('data-service:reset', async () => {
const result = await deps.resetDataService();
if (!result.success || !result.data) throw new Error(result.error || '开发数据重置失败');
if (generation !== loadGeneration || get().projectId !== projectId) return;
set({ dataService: result.data });
if (!isCurrentProject(projectId)) return;
advanceProjectEpoch(projectId);
set({ dataService: result.data, loadState: 'ready' });
});
},
removeCollection(collection) {
const projectId = get().projectId;
const generation = loadGeneration;
return operation(`data-service:collection:${collection}`, async () => {
const result = await deps.removeCollection(collection);
if (!result.success) throw new Error(result.error || '移除 collection 失败');
if (generation !== loadGeneration || get().projectId !== projectId) return;
if (!isCurrentProject(projectId)) return;
const inspected = await deps.inspectDataService();
if (generation !== loadGeneration || get().projectId !== projectId) return;
if (inspected.success && inspected.data) set({ dataService: inspected.data });
if (!isCurrentProject(projectId)) return;
advanceProjectEpoch(projectId);
if (inspected.success && inspected.data) {
set({ dataService: inspected.data, loadState: 'ready' });
} else {
await get().load(projectId);
}
});
},
removeProject() {
const projectId = get().projectId;
const generation = loadGeneration;
return operation('data-service:remove-project', async () => {
const result = await deps.removeProject();
if (!result.success) throw new Error(result.error || '删除开发数据空间失败');
if (generation !== loadGeneration || get().projectId !== projectId) return;
set({ dataService: null });
if (projectId) await get().load(projectId);
if (!isCurrentProject(projectId)) return;
advanceProjectEpoch(projectId);
set({ dataService: null, loadState: 'ready' });
await get().load(projectId);
});
},
}));