200 lines
8.2 KiB
TypeScript
200 lines
8.2 KiB
TypeScript
import { useStore } from 'zustand';
|
|
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
import {
|
|
configureDataService,
|
|
getCodingPlugins,
|
|
inspectDataService,
|
|
removeDataServiceCollection,
|
|
removeDataServiceProject,
|
|
resetDataService,
|
|
setCodingPluginEnabled,
|
|
type CodingPluginProject,
|
|
} from '@/lib/coding-plugins';
|
|
import type { DataServiceInstanceState } from '../../shared/data-service';
|
|
|
|
type Dependencies = {
|
|
list: typeof getCodingPlugins;
|
|
setEnabled: typeof setCodingPluginEnabled;
|
|
inspectDataService: typeof inspectDataService;
|
|
configureDataService: typeof configureDataService;
|
|
resetDataService: typeof resetDataService;
|
|
removeCollection: typeof removeDataServiceCollection;
|
|
removeProject: typeof removeDataServiceProject;
|
|
};
|
|
|
|
export type CodingPluginsState = {
|
|
projectId: string | null;
|
|
projection: CodingPluginProject | null;
|
|
dataService: DataServiceInstanceState | null;
|
|
loadState: 'idle' | 'loading' | 'ready' | 'error';
|
|
error: string | null;
|
|
pending: Record<string, true>;
|
|
load(projectId: string): Promise<void>;
|
|
setEnabled(projectId: string, pluginId: string, enabled: boolean): Promise<void>;
|
|
configure(collections: string[]): Promise<void>;
|
|
reset(): Promise<void>;
|
|
removeCollection(collection: string): Promise<void>;
|
|
removeProject(): Promise<void>;
|
|
};
|
|
|
|
export function createCodingPluginsStore(overrides: Partial<Dependencies> = {}): StoreApi<CodingPluginsState> {
|
|
const deps: Dependencies = {
|
|
list: getCodingPlugins, setEnabled: setCodingPluginEnabled, inspectDataService,
|
|
configureDataService, resetDataService, removeCollection: removeDataServiceCollection,
|
|
removeProject: removeDataServiceProject, ...overrides,
|
|
};
|
|
const flights = new Map<string, Promise<void>>();
|
|
const loadFlightGenerations = new Map<string, number>();
|
|
const pendingCounts = new Map<string, number>();
|
|
const projectEpochs = new Map<string, number>();
|
|
let loadGeneration = 0;
|
|
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(() => {
|
|
if (flights.get(key) === flight) {
|
|
flights.delete(key);
|
|
loadFlightGenerations.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);
|
|
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) {
|
|
latestLoadProjectId = projectId;
|
|
const epoch = projectEpoch(projectId);
|
|
const pendingKey = `load:${projectId}`;
|
|
const key = `${pendingKey}:${epoch}`;
|
|
const existing = flights.get(key);
|
|
if (existing) {
|
|
loadFlightGenerations.set(key, ++loadGeneration);
|
|
return existing;
|
|
}
|
|
loadFlightGenerations.set(key, ++loadGeneration);
|
|
return operation(key, async () => {
|
|
set({ loadState: 'loading', error: null });
|
|
try {
|
|
const projection = await deps.list(projectId);
|
|
let dataService = get().projectId === projectId ? get().dataService : null;
|
|
const item = projection.items.find(({ settingsSurface }) => settingsSurface === 'data-service');
|
|
if (item?.enabled && item.backend.status === 'ready') {
|
|
const result = await deps.inspectDataService();
|
|
if (result.success && result.data) dataService = result.data;
|
|
}
|
|
if (loadFlightGenerations.get(key) === loadGeneration && epoch === projectEpoch(projectId)) {
|
|
set({ projectId, projection, dataService, loadState: 'ready', error: null });
|
|
}
|
|
} catch (error) {
|
|
if (loadFlightGenerations.get(key) === loadGeneration && epoch === projectEpoch(projectId)) {
|
|
set({ loadState: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
throw error;
|
|
}
|
|
}, pendingKey);
|
|
},
|
|
setEnabled(projectId, pluginId, enabled) {
|
|
return operation(`enabled:${projectId}:${pluginId}`, async () => {
|
|
const projection = await deps.setEnabled(projectId, pluginId, enabled);
|
|
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 (!isCurrentProject(projectId)) return;
|
|
if (inspected.success && inspected.data) dataService = inspected.data;
|
|
}
|
|
advanceProjectEpoch(projectId);
|
|
set({
|
|
projectId, projection, dataService, loadState: 'ready', error: null,
|
|
});
|
|
});
|
|
},
|
|
configure(collections) {
|
|
const projectId = get().projectId;
|
|
return operation('data-service:configure', async () => {
|
|
const result = await deps.configureDataService(collections);
|
|
if (!result.success || !result.data) throw new Error(result.error || '开发数据空间创建失败');
|
|
if (!isCurrentProject(projectId)) return;
|
|
advanceProjectEpoch(projectId);
|
|
set({ dataService: result.data, loadState: 'ready', error: null });
|
|
await get().load(projectId);
|
|
});
|
|
},
|
|
reset() {
|
|
const projectId = get().projectId;
|
|
return operation('data-service:reset', async () => {
|
|
const result = await deps.resetDataService();
|
|
if (!result.success || !result.data) throw new Error(result.error || '开发数据重置失败');
|
|
if (!isCurrentProject(projectId)) return;
|
|
advanceProjectEpoch(projectId);
|
|
set({ dataService: result.data, loadState: 'ready' });
|
|
});
|
|
},
|
|
removeCollection(collection) {
|
|
const projectId = get().projectId;
|
|
return operation(`data-service:collection:${collection}`, async () => {
|
|
const result = await deps.removeCollection(collection);
|
|
if (!result.success) throw new Error(result.error || '移除 collection 失败');
|
|
if (!isCurrentProject(projectId)) return;
|
|
const inspected = await deps.inspectDataService();
|
|
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;
|
|
return operation('data-service:remove-project', async () => {
|
|
const result = await deps.removeProject();
|
|
if (!result.success) throw new Error(result.error || '删除开发数据空间失败');
|
|
if (!isCurrentProject(projectId)) return;
|
|
advanceProjectEpoch(projectId);
|
|
set({ dataService: null, loadState: 'ready' });
|
|
await get().load(projectId);
|
|
});
|
|
},
|
|
}));
|
|
return store;
|
|
}
|
|
|
|
export const codingPluginsStore = createCodingPluginsStore();
|
|
export function useCodingPluginsStore<T>(selector: (state: CodingPluginsState) => T): T {
|
|
return useStore(codingPluginsStore, selector);
|
|
}
|