fix: complete marketplace client remediation

This commit is contained in:
2026-08-28 23:14:41 +08:00
parent 2c3baf6dff
commit 11d0af0166
18 changed files with 1007 additions and 90 deletions

View File

@@ -91,10 +91,64 @@ export function createPluginMarketplaceStore(
const deps = { ...defaults, ...overrides };
let scopeGeneration = 0;
let catalogGeneration = 0;
let libraryMutationEpoch = 0;
let libraryReadGeneration = 0;
let libraryIntent = 0;
let latestLibraryReadIntent = 0;
const latestPluginMutationIntent = new Map<string, number>();
return createStore<PluginMarketplaceState>((set, get) => {
const beginMutation = (pluginId: string): number => {
const intent = ++libraryIntent;
latestPluginMutationIntent.set(pluginId, intent);
return intent;
};
const canCommitMutation = (
pluginId: string,
intent: number,
generation: number,
accountKey: string,
): boolean => generation === scopeGeneration
&& get().accountKey === accountKey
&& latestPluginMutationIntent.get(pluginId) === intent
// A later explicit read is the newest account projection for every
// plugin, so an older mutation must not replace it.
&& latestLibraryReadIntent <= intent;
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.get(left.pluginId) ?? 0;
const rightIntent = latestPluginMutationIntent.get(right.pluginId) ?? 0;
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<PluginMarketplaceState, 'library' | 'installations'> => {
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 <T>(key: string, operation: () => Promise<T>): Promise<T> => {
set((state) => ({ pending: { ...state.pending, [key]: true } }));
try {
@@ -116,21 +170,19 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理我的插件');
const mutationEpoch = ++libraryMutationEpoch;
const intent = beginMutation(pluginId);
try {
const projection = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey
|| mutationEpoch !== libraryMutationEpoch) return;
libraryMutationEpoch += 1;
if (!canCommitMutation(pluginId, intent, generation, accountKey)) return;
const merged = mergeLibraryProjection(pluginId, projection);
set({
library: projection.library,
installations: installationMap(projection.installations),
library: merged.library,
installations: merged.installations,
libraryState: 'ready',
libraryError: null,
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey
&& mutationEpoch === libraryMutationEpoch) {
if (canCommitMutation(pluginId, intent, generation, accountKey)) {
set({ libraryError: message(error) });
}
throw error;
@@ -145,12 +197,10 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理设备插件');
const mutationEpoch = ++libraryMutationEpoch;
const intent = beginMutation(pluginId);
try {
const result = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey
|| mutationEpoch !== libraryMutationEpoch) return;
libraryMutationEpoch += 1;
if (!canCommitMutation(pluginId, intent, generation, accountKey)) return;
set((state) => {
const installations = { ...state.installations };
if (result.status === 'removed') delete installations[pluginId];
@@ -158,8 +208,7 @@ export function createPluginMarketplaceStore(
return { installations, libraryError: null };
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey
&& mutationEpoch === libraryMutationEpoch) {
if (canCommitMutation(pluginId, intent, generation, accountKey)) {
const reason = message(error);
set((state) => ({
installations: {
@@ -199,8 +248,9 @@ export function createPluginMarketplaceStore(
activateAccount(accountKey) {
if (get().accountKey === accountKey) return;
scopeGeneration += 1;
libraryReadGeneration += 1;
libraryMutationEpoch += 1;
libraryIntent += 1;
latestLibraryReadIntent = libraryIntent;
latestPluginMutationIntent.clear();
set({
accountKey,
library: null,
@@ -242,10 +292,10 @@ export function createPluginMarketplaceStore(
}
},
async loadLibrary() {
const readGeneration = ++libraryReadGeneration;
const readIntent = ++libraryIntent;
latestLibraryReadIntent = readIntent;
const generation = scopeGeneration;
const accountKey = get().accountKey;
const mutationEpoch = libraryMutationEpoch;
if (!accountKey) {
set({ library: null, installations: {}, libraryState: 'idle', libraryError: null });
return;
@@ -254,8 +304,8 @@ export function createPluginMarketplaceStore(
try {
const projection = await deps.readLibrary();
if (generation === scopeGeneration && get().accountKey === accountKey
&& readGeneration === libraryReadGeneration
&& mutationEpoch === libraryMutationEpoch) {
&& readIntent === latestLibraryReadIntent
&& readIntent === libraryIntent) {
set({
library: projection.library,
installations: installationMap(projection.installations),
@@ -265,8 +315,8 @@ export function createPluginMarketplaceStore(
}
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey
&& readGeneration === libraryReadGeneration
&& mutationEpoch === libraryMutationEpoch) {
&& readIntent === latestLibraryReadIntent
&& readIntent === libraryIntent) {
set({ libraryState: 'error', libraryError: message(error) });
}
throw error;