mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-23 09:19:06 +00:00
fix(properties): harden lifecycle and session fencing
- Fence Properties actions and Torrent moves by current caller sessions. - Preserve move progress and authoritative destinations across stale events and recovery. - Enforce immutable identity fields and transactional queued-edit rejection. - Restore subtle theme surfaces and expand regression coverage.
This commit is contained in:
@@ -1060,23 +1060,53 @@ interface DownloadState {
|
||||
export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
const applyPropertiesInternal = async (id: string, updates: Partial<DownloadItem>): Promise<void> => {
|
||||
await waitForPendingStartupResume();
|
||||
const initialItem = get().downloads.find(download => download.id === id);
|
||||
if (!initialItem) return;
|
||||
// Reject immutable identity edits before invalidating a live queued
|
||||
// dispatch. Validation after invalidation would cancel a legitimate
|
||||
// enqueue even though no mutation was accepted.
|
||||
if ((updates.fileName !== undefined || updates.destination !== undefined)
|
||||
&& (initialItem.isTorrent === true || !['ready', 'staged'].includes(initialItem.status))) {
|
||||
throw new Error(i18n.t($ => $.properties.identityReadOnly));
|
||||
}
|
||||
const wasDispatching = await invalidateAndWaitForDispatch(id);
|
||||
const state = get();
|
||||
const item = state.downloads.find(d => d.id === id);
|
||||
if (!item) return;
|
||||
const previousItem = item;
|
||||
const previousPendingOrder = [...state.pendingOrder];
|
||||
const wasBackendRegistered = state.backendRegisteredIds.has(id);
|
||||
const commitProperties = async (): Promise<void> => {
|
||||
try {
|
||||
await commitDownloadState();
|
||||
} catch (error) {
|
||||
// Do not leave a renderer-only Properties edit that will disappear on
|
||||
// restart. Restore the prior row while retaining the native lifecycle
|
||||
// fencing already performed for this operation.
|
||||
// A queued item may already have been detached before persistence
|
||||
// failed. Restore both projections, then rebuild the backend lifecycle
|
||||
// when the previous row owned one; restoring only the React object
|
||||
// would leave a visible queued row that can never dispatch.
|
||||
set(current => ({
|
||||
downloads: current.downloads.map(download =>
|
||||
download.id === id ? previousItem : download
|
||||
)
|
||||
downloads: current.downloads.map(download => download.id === id ? previousItem : download),
|
||||
pendingOrder: previousPendingOrder,
|
||||
backendRegisteredIds: new Set(
|
||||
[...current.backendRegisteredIds].filter(registeredId => registeredId !== id)
|
||||
),
|
||||
}));
|
||||
if ((wasBackendRegistered || wasDispatching) && previousItem.status === 'queued') {
|
||||
const restored = await dispatchItemInternal(id);
|
||||
if (!restored) {
|
||||
set(current => ({
|
||||
downloads: current.downloads.map(download =>
|
||||
download.id === id
|
||||
? { ...previousItem, status: 'failed' as const, hasBeenDispatched: false, lastError: errorMessage(error) }
|
||||
: download
|
||||
),
|
||||
pendingOrder: current.pendingOrder.filter(value => value !== id),
|
||||
backendRegisteredIds: new Set(
|
||||
[...current.backendRegisteredIds].filter(registeredId => registeredId !== id)
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1100,6 +1130,11 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
&& normalizedUpdates.torrentRemoveUnselectedFile === false
|
||||
&& item.torrentRemoveUnselectedFile !== false;
|
||||
|
||||
if ((normalizedUpdates.fileName !== undefined || normalizedUpdates.destination !== undefined)
|
||||
&& (item.isTorrent === true || !['ready', 'staged'].includes(item.status))) {
|
||||
throw new Error(i18n.t($ => $.properties.identityReadOnly));
|
||||
}
|
||||
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'verifying' || item.status === 'seeding' || item.status === 'retrying') {
|
||||
throw new Error(i18n.t($ => $.downloadTable.transferActive));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user