fix(accounts): stabilize extreme toggle bursts
This commit is contained in:
+38
-34
@@ -41,7 +41,7 @@ import {
|
||||
} from "../shared/provider-daily-limits";
|
||||
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
||||
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection";
|
||||
import { buildConfiguredProviderOrder, createAccountToggleQueue, enqueueAccountToggleIntent, filterAccountDialogOptions, getAccountDialogSelectableOptions, getAvailableAccountOptions, mergeAccountToggleSettings, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, sortAccountServices, updateAccountRowSelection, type AccountToggleTarget } from "./account-ui";
|
||||
import { buildConfiguredProviderOrder, createAccountToggleQueue, enqueueAccountToggleIntent, filterAccountDialogOptions, getAccountDialogSelectableOptions, getAvailableAccountOptions, mergeAccountToggleSettings, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountToggleIntentEnabled, resolveAccountUsername, resolveVisibleAccountKind, sortAccountServices, updateAccountRowSelection, type AccountToggleTarget } from "./account-ui";
|
||||
import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
|
||||
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
||||
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
|
||||
@@ -1803,6 +1803,7 @@ export function App(): ReactElement {
|
||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||
const [accountDialogServiceFilter, setAccountDialogServiceFilter] = useState("all");
|
||||
const [pendingAccountToggles, setPendingAccountToggles] = useState<Record<string, { enabled: boolean; sequence: number }>>({});
|
||||
const pendingAccountTogglesRef = useRef<Record<string, { enabled: boolean; sequence: number }>>({});
|
||||
const accountToggleQueueRef = useRef(createAccountToggleQueue());
|
||||
const accountToggleSequenceRef = useRef(0);
|
||||
const settingsMutationSequenceRef = useRef(0);
|
||||
@@ -3184,10 +3185,11 @@ export function App(): ReactElement {
|
||||
|
||||
const requestAccountToggle = (row: AccountTableRow, enabled: boolean): void => {
|
||||
const sequence = ++accountToggleSequenceRef.current;
|
||||
const requestedEnabled = resolveAccountToggleIntentEnabled(pendingAccountTogglesRef.current[row.rowKey]?.enabled, enabled);
|
||||
const subject = row.toggleKind === "dl" && row.dlKey
|
||||
? `${row.entry.serviceLabel} ${row.dlKey.label}`
|
||||
: row.entry.serviceLabel;
|
||||
const check = enabled
|
||||
const check = requestedEnabled
|
||||
? row.toggleKind === "rd" && row.accountId
|
||||
? () => window.rd.checkAccountCredentials({ kind: row.entry.kind as "realdebrid-api" | "realdebrid-web", accountId: row.accountId || undefined })
|
||||
: row.toggleKind === "mega" && row.accountId
|
||||
@@ -3198,14 +3200,16 @@ export function App(): ReactElement {
|
||||
? () => window.rd.checkAccountCredentials({ kind: "deepbrid-api", accountId: "svc-deepbrid" })
|
||||
: undefined
|
||||
: undefined;
|
||||
setPendingAccountToggles((current) => ({
|
||||
...current,
|
||||
[row.rowKey]: { enabled, sequence }
|
||||
}));
|
||||
const nextPending = {
|
||||
...pendingAccountTogglesRef.current,
|
||||
[row.rowKey]: { enabled: requestedEnabled, sequence }
|
||||
};
|
||||
pendingAccountTogglesRef.current = nextPending;
|
||||
setPendingAccountToggles(nextPending);
|
||||
void enqueueAccountToggleIntent(accountToggleQueueRef.current, {
|
||||
key: row.rowKey,
|
||||
target: getAccountToggleTarget(row),
|
||||
enabled,
|
||||
enabled: requestedEnabled,
|
||||
check
|
||||
}, {
|
||||
getSettings: () => persistedSettingsRef.current,
|
||||
@@ -3218,17 +3222,16 @@ export function App(): ReactElement {
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.status === "superseded") return;
|
||||
setPendingAccountToggles((current) => {
|
||||
if (current[row.rowKey]?.sequence !== sequence) return current;
|
||||
const next = { ...current };
|
||||
delete next[row.rowKey];
|
||||
return next;
|
||||
});
|
||||
if (pendingAccountTogglesRef.current[row.rowKey]?.sequence !== sequence) return;
|
||||
const settledPending = { ...pendingAccountTogglesRef.current };
|
||||
delete settledPending[row.rowKey];
|
||||
pendingAccountTogglesRef.current = settledPending;
|
||||
setPendingAccountToggles(settledPending);
|
||||
if (result.status === "failed") {
|
||||
showToast(`${subject}: Umschalten fehlgeschlagen: ${String(result.error)}`, 3200);
|
||||
return;
|
||||
}
|
||||
showToast(`${subject} ${enabled ? "aktiviert" : "deaktiviert"}`, 2200);
|
||||
showToast(`${subject} ${requestedEnabled ? "aktiviert" : "deaktiviert"}`, 2200);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4543,21 +4546,20 @@ export function App(): ReactElement {
|
||||
};
|
||||
|
||||
const onImportBackup = async (): Promise<void> => {
|
||||
closeMenus();
|
||||
await performQuickAction(async () => {
|
||||
const result = await runLocalBackupImport(window.rd, askBackupPassphrase);
|
||||
if (result.restored) {
|
||||
showToast(result.message, 4000);
|
||||
// A settings-only import applies live without a relaunch, so the editable
|
||||
// settings form would otherwise keep showing the old values. Pull the
|
||||
// fresh settings and re-seed the draft so the UI reflects the import.
|
||||
if (!result.relaunch) {
|
||||
const fresh = await window.rd.getSnapshot();
|
||||
applyPersistedSettings(fresh.settings, false);
|
||||
}
|
||||
} else if (result.message !== "Abgebrochen") {
|
||||
showToast(`Sicherung laden fehlgeschlagen: ${result.message}`, 3000);
|
||||
}
|
||||
closeMenus();
|
||||
await performQuickAction(async () => {
|
||||
await runQueuedSettingsMutation(async () => {
|
||||
const result = await runLocalBackupImport(window.rd, askBackupPassphrase);
|
||||
if (result.restored) {
|
||||
showToast(result.message, 4000);
|
||||
if (!result.relaunch) {
|
||||
const fresh = await window.rd.getSnapshot();
|
||||
applyPersistedSettings(fresh.settings, false);
|
||||
}
|
||||
} else if (result.message !== "Abgebrochen") {
|
||||
showToast(`Sicherung laden fehlgeschlagen: ${result.message}`, 3000);
|
||||
}
|
||||
});
|
||||
}, (error) => {
|
||||
showToast(`Sicherung laden fehlgeschlagen: ${String(error)}`, 2600);
|
||||
});
|
||||
@@ -4585,11 +4587,13 @@ export function App(): ReactElement {
|
||||
if (!key) return;
|
||||
setOnlineBackupDialog((current) => current ? { ...current, busy: true, error: "" } : current);
|
||||
try {
|
||||
const result = await window.rd.importOnlineBackup(key);
|
||||
const fresh = await window.rd.getSnapshot();
|
||||
applyPersistedSettings(fresh.settings, false);
|
||||
setOnlineBackupDialog(null);
|
||||
showToast(result.message, 4000);
|
||||
await runQueuedSettingsMutation(async () => {
|
||||
const result = await window.rd.importOnlineBackup(key);
|
||||
const fresh = await window.rd.getSnapshot();
|
||||
applyPersistedSettings(fresh.settings, false);
|
||||
setOnlineBackupDialog(null);
|
||||
showToast(result.message, 4000);
|
||||
});
|
||||
} catch {
|
||||
setOnlineBackupDialog((current) => current ? { ...current, busy: false, error: "Online-Sicherung konnte nicht geladen werden. Schlüssel prüfen und erneut versuchen." } : current);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ export interface AccountToggleQueue {
|
||||
enqueue<T>(key: string, task: (isCurrent: () => boolean) => Promise<T>): Promise<AccountToggleQueueResult<T>>;
|
||||
}
|
||||
|
||||
export function resolveAccountToggleIntentEnabled(pendingEnabled: boolean | undefined, eventEnabled: boolean): boolean {
|
||||
return pendingEnabled === undefined ? eventEnabled : !pendingEnabled;
|
||||
}
|
||||
|
||||
export function createAccountToggleQueue(): AccountToggleQueue {
|
||||
let tail = Promise.resolve();
|
||||
const versions = new Map<string, number>();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
matchesAccountModeFilter,
|
||||
mergeAccountToggleSettings,
|
||||
pruneAccountRowSelection,
|
||||
resolveAccountToggleIntentEnabled,
|
||||
resolveAccountUsername,
|
||||
resolveVisibleAccountKind,
|
||||
sortAccountServices
|
||||
@@ -143,6 +144,12 @@ describe("account usernames", () => {
|
||||
});
|
||||
|
||||
describe("account toggle bursts", () => {
|
||||
it("toggles from the synchronous pending intent when the controlled DOM value lags", () => {
|
||||
expect(resolveAccountToggleIntentEnabled(undefined, false)).toBe(false);
|
||||
expect(resolveAccountToggleIntentEnabled(false, false)).toBe(true);
|
||||
expect(resolveAccountToggleIntentEnabled(true, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("serializes different account intents without dropping the second task", async () => {
|
||||
const queue = createAccountToggleQueue();
|
||||
const events: string[] = [];
|
||||
|
||||
@@ -1356,6 +1356,16 @@ describe("account workspace", () => {
|
||||
});
|
||||
|
||||
describe("settings App integration", () => {
|
||||
it("serializes local and online backup imports with pending account mutations", () => {
|
||||
const localImport = sourceBlock(appSource, "const onImportBackup", "const onCreateOnlineBackup");
|
||||
const onlineImport = sourceBlock(appSource, "const onImportOnlineBackup", "const onCopyOnlineBackupKey");
|
||||
|
||||
expect(localImport).toContain("runQueuedSettingsMutation");
|
||||
expect(onlineImport).toContain("runQueuedSettingsMutation");
|
||||
expect(localImport.indexOf("runQueuedSettingsMutation")).toBeLessThan(localImport.indexOf("runLocalBackupImport"));
|
||||
expect(onlineImport.indexOf("runQueuedSettingsMutation")).toBeLessThan(onlineImport.indexOf("window.rd.importOnlineBackup"));
|
||||
});
|
||||
|
||||
it("projects provider order entries with logos and explicit login modes", () => {
|
||||
const settings = createRendererSettings({
|
||||
...defaultSettings(),
|
||||
|
||||
Reference in New Issue
Block a user