import { createStore, type StoreApi } from 'zustand'; import { useStore } from 'zustand'; import { acquireMarketplacePlugin, installBetaMarketplacePlugin, installMarketplacePlugin, readMarketplaceCatalog, readMarketplaceDetail, readMarketplaceLibrary, removeMarketplacePlugin, uninstallMarketplacePlugin, updateMarketplacePlugin, type MarketplaceCatalogPage, type MarketplaceCatalogQuery, type MarketplaceInstallation, type MarketplaceLibraryProjection, type MarketplaceLibrarySnapshot, type MarketplacePluginDetail, } from '@/lib/plugin-marketplace'; type LoadState = 'idle' | 'loading' | 'ready' | 'error'; export type PluginMarketplaceDependencies = { readCatalog(input?: MarketplaceCatalogQuery): Promise; readDetail(pluginId: string): Promise; readLibrary(): Promise; acquire(pluginId: string): Promise; remove(pluginId: string): Promise; install(pluginId: string): Promise; installBeta(pluginId: string): Promise; update(pluginId: string): Promise; uninstall(pluginId: string): Promise; }; export type PluginMarketplaceState = { accountKey: string | null; catalog: MarketplaceCatalogPage | null; catalogState: LoadState; catalogError: string | null; catalogQuery: MarketplaceCatalogQuery; library: MarketplaceLibrarySnapshot | null; libraryState: LoadState; libraryError: string | null; installations: Record; details: Record; detailState: Record; pending: Record; activateAccount(accountKey: string | null): void; loadCatalog(input?: MarketplaceCatalogQuery): Promise; loadDetail(pluginId: string): Promise; loadLibrary(): Promise; acquire(pluginId: string): Promise; remove(pluginId: string): Promise; install(pluginId: string): Promise; installBeta(pluginId: string): Promise; update(pluginId: string): Promise; uninstall(pluginId: string): Promise; }; const defaults: PluginMarketplaceDependencies = { readCatalog: readMarketplaceCatalog, readDetail: readMarketplaceDetail, readLibrary: readMarketplaceLibrary, acquire: acquireMarketplacePlugin, remove: removeMarketplacePlugin, install: installMarketplacePlugin, installBeta: installBetaMarketplacePlugin, update: updateMarketplacePlugin, uninstall: uninstallMarketplacePlugin, }; function message(error: unknown): string { const base = error instanceof Error ? error.message : String(error); if (!error || typeof error !== 'object') return base; const errorCode = 'code' in error && typeof error.code === 'string' ? error.code : null; const details = 'details' in error && error.details && typeof error.details === 'object' ? error.details as Record : null; const backendCode = details?.backendCode; const prefixed = typeof backendCode === 'string' ? `${backendCode}: ${base}` : base; return errorCode && !prefixed.includes(errorCode) ? `${errorCode}: ${prefixed}` : prefixed; } function installationMap(items: readonly MarketplaceInstallation[]): Record { return Object.fromEntries(items.map((item) => [item.pluginId, item])); } export function createPluginMarketplaceStore( overrides: Partial = {}, ): StoreApi { const deps = { ...defaults, ...overrides }; let scopeGeneration = 0; let catalogGeneration = 0; let libraryIntent = 0; let latestLibraryReadIntent = 0; const latestLibraryMutationIntent = new Map(); const latestDeviceMutationIntent = new Map(); return createStore((set, get) => { const beginMutation = (pluginId: string, domain: 'library' | 'device'): number => { const intent = ++libraryIntent; (domain === 'library' ? latestLibraryMutationIntent : latestDeviceMutationIntent).set(pluginId, intent); return intent; }; const canCommitMutation = ( pluginId: string, intent: number, generation: number, accountKey: string, domain: 'library' | 'device', ): boolean => generation === scopeGeneration && get().accountKey === accountKey && (domain === 'library' ? latestLibraryMutationIntent : latestDeviceMutationIntent).get(pluginId) === intent // A later explicit read is the newest Library projection, so an older // Library mutation must not replace it. Device state is independent. && (domain === 'device' || latestLibraryReadIntent <= intent); const latestPluginMutationIntent = (pluginId: string): number => Math.max( latestLibraryMutationIntent.get(pluginId) ?? 0, latestDeviceMutationIntent.get(pluginId) ?? 0, ); const mergeLibraryForPlugin = ( current: MarketplaceLibrarySnapshot | null, incoming: MarketplaceLibrarySnapshot, pluginId: string, ): MarketplaceLibrarySnapshot => { if (!current) return incoming; const byPlugin = new Map(current.items.map((item) => [item.pluginId, item])); const incomingItem = incoming.items.find((item) => item.pluginId === pluginId); if (incomingItem) byPlugin.set(pluginId, incomingItem); else byPlugin.delete(pluginId); const items = [...byPlugin.values()].sort((left, right) => { const leftIntent = latestPluginMutationIntent(left.pluginId); const rightIntent = latestPluginMutationIntent(right.pluginId); return rightIntent - leftIntent; }); return { ...incoming, items, total: Math.max(current.total, incoming.total, items.length), fetchedAt: Math.max(current.fetchedAt, incoming.fetchedAt), }; }; const mergeLibraryProjection = ( pluginId: string, projection: MarketplaceLibraryProjection, ): Pick => { const state = get(); const library = mergeLibraryForPlugin(state.library, projection.library, pluginId); const incomingInstallation = projection.installations.find((item) => item.pluginId === pluginId); const installations = { ...state.installations }; if (incomingInstallation) installations[pluginId] = incomingInstallation; return { library, installations }; }; const pending = async (key: string, operation: () => Promise): Promise => { set((state) => ({ pending: { ...state.pending, [key]: true } })); try { return await operation(); } finally { set((state) => { const next = { ...state.pending }; delete next[key]; return { pending: next }; }); } }; const libraryAction = async ( key: string, pluginId: string, action: (pluginId: string) => Promise, ): Promise => pending(key, async () => { const generation = scopeGeneration; const accountKey = get().accountKey; if (!accountKey) throw new Error('请先登录后管理我的插件'); const intent = beginMutation(pluginId, 'library'); try { const projection = await action(pluginId); if (!canCommitMutation(pluginId, intent, generation, accountKey, 'library')) return; const merged = mergeLibraryProjection(pluginId, projection); set({ library: merged.library, installations: merged.installations, libraryState: 'ready', libraryError: null, }); } catch (error) { if (canCommitMutation(pluginId, intent, generation, accountKey, 'library')) { set({ libraryError: message(error) }); } throw error; } }); const installAction = async ( key: string, pluginId: string, action: (pluginId: string) => Promise, ): Promise => pending(key, async () => { const generation = scopeGeneration; const accountKey = get().accountKey; if (!accountKey) throw new Error('请先登录后管理设备插件'); const intent = beginMutation(pluginId, 'device'); try { const result = await action(pluginId); if (!canCommitMutation(pluginId, intent, generation, accountKey, 'device')) return; set((state) => { const installations = { ...state.installations }; if (result.status === 'removed') delete installations[pluginId]; else installations[pluginId] = result; return { installations, libraryError: null }; }); } catch (error) { if (canCommitMutation(pluginId, intent, generation, accountKey, 'device')) { const reason = message(error); set((state) => ({ installations: { ...state.installations, [pluginId]: { status: 'unavailable', pluginId, ...(state.installations[pluginId]?.releaseId ? { releaseId: state.installations[pluginId].releaseId } : {}), ...(state.installations[pluginId]?.version ? { version: state.installations[pluginId].version } : {}), reason, }, }, libraryError: reason, })); } throw error; } }); return { accountKey: null, catalog: null, catalogState: 'idle', catalogError: null, catalogQuery: {}, library: null, libraryState: 'idle', libraryError: null, installations: {}, details: {}, detailState: {}, pending: {}, activateAccount(accountKey) { if (get().accountKey === accountKey) return; scopeGeneration += 1; libraryIntent += 1; latestLibraryReadIntent = libraryIntent; latestLibraryMutationIntent.clear(); latestDeviceMutationIntent.clear(); set({ accountKey, library: null, libraryState: 'idle', libraryError: null, installations: {}, pending: {}, }); }, async loadCatalog(input = {}) { const generation = ++catalogGeneration; set({ catalogState: 'loading', catalogError: null, catalogQuery: input }); try { const catalog = await deps.readCatalog(input); if (generation === catalogGeneration) set({ catalog, catalogState: 'ready', catalogError: null }); } catch (error) { if (generation === catalogGeneration) set({ catalogState: 'error', catalogError: message(error) }); throw error; } }, async loadDetail(pluginId) { const generation = scopeGeneration; set((state) => ({ detailState: { ...state.detailState, [pluginId]: 'loading' } })); try { const detail = await deps.readDetail(pluginId); if (generation !== scopeGeneration) return; set((state) => ({ details: { ...state.details, [pluginId]: detail }, detailState: { ...state.detailState, [pluginId]: 'ready' }, })); } catch (error) { if (generation === scopeGeneration) { set((state) => ({ detailState: { ...state.detailState, [pluginId]: 'error' }, catalogError: message(error), })); } throw error; } }, async loadLibrary() { const readIntent = ++libraryIntent; latestLibraryReadIntent = readIntent; const generation = scopeGeneration; const accountKey = get().accountKey; if (!accountKey) { set({ library: null, installations: {}, libraryState: 'idle', libraryError: null }); return; } set({ libraryState: 'loading', libraryError: null }); try { const projection = await deps.readLibrary(); if (generation === scopeGeneration && get().accountKey === accountKey && readIntent === latestLibraryReadIntent && readIntent === libraryIntent) { set({ library: projection.library, installations: installationMap(projection.installations), libraryState: 'ready', libraryError: null, }); } } catch (error) { if (generation === scopeGeneration && get().accountKey === accountKey && readIntent === latestLibraryReadIntent && readIntent === libraryIntent) { set({ libraryState: 'error', libraryError: message(error) }); } throw error; } }, acquire: (pluginId) => libraryAction(`acquire:${pluginId}`, pluginId, deps.acquire), remove: (pluginId) => libraryAction(`remove:${pluginId}`, pluginId, deps.remove), install: (pluginId) => installAction(`install:${pluginId}`, pluginId, deps.install), installBeta: (pluginId) => installAction(`install-beta:${pluginId}`, pluginId, deps.installBeta), update: (pluginId) => installAction(`update:${pluginId}`, pluginId, deps.update), uninstall: (pluginId) => installAction(`uninstall:${pluginId}`, pluginId, deps.uninstall), }; }); } export const pluginMarketplaceStore = createPluginMarketplaceStore(); export function usePluginMarketplaceStore(selector: (state: PluginMarketplaceState) => T): T { return useStore(pluginMarketplaceStore, selector); }