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:
NimBold
2026-08-12 07:07:52 +03:30
parent f7bafdeb0e
commit 64a836f09f
20 changed files with 488 additions and 115 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ describe('download action policy', () => {
{ status: 'completed' },
]);
expect(counts).toEqual({ pause: 3, resume: 4 });
expect(counts).toEqual({ pause: 3, resume: 3 });
});
it('keeps large action badges compact without changing the accessible count', () => {
+4 -1
View File
@@ -45,7 +45,10 @@ export const countDownloadActions = (
downloads: ReadonlyArray<{ status: DownloadStatus }>
): DownloadActionCounts => downloads.reduce<DownloadActionCounts>((counts, download) => {
if (canPauseDownload(download.status)) counts.pause += 1;
if (canStartDownload(download.status)) counts.resume += 1;
if (download.status === 'paused'
|| (canStartDownload(download.status) && !canPauseDownload(download.status))) {
counts.resume += 1;
}
return counts;
}, { pause: 0, resume: 0 });
+27 -1
View File
@@ -1,7 +1,33 @@
import { describe, expect, it } from 'vitest';
import { getPropertiesConnectionPresentation } from './propertiesPresentation';
import { getPropertiesConnectionPresentation, getPropertiesProgress } from './propertiesPresentation';
describe('Properties connection presentation', () => {
it('uses move progress instead of the completed download fraction during relocation', () => {
expect(getPropertiesProgress({
status: 'moving',
moveProgress: 0.42,
fraction: 1,
downloadedBytes: 100,
totalBytes: 100,
totalIsEstimate: false,
isMedia: false,
size: '100 B',
})).toBe(0.42);
expect(getPropertiesProgress({
status: 'moving',
moveProgress: 4,
fraction: 0,
isMedia: false,
})).toBe(1);
expect(getPropertiesProgress({
status: 'moving',
fraction: 1,
downloadedBytes: 100,
totalBytes: 100,
isMedia: false,
})).toBe(0);
});
it('keeps media concurrency out of the live header metrics', () => {
expect(getPropertiesConnectionPresentation({
isMedia: true,
+7
View File
@@ -1,4 +1,5 @@
import type { PropertiesSnapshot } from '../propertiesBridge';
import { resolveDownloadFraction } from './downloadProgress';
export type PropertiesConnectionKind = 'media' | 'torrent' | 'aria2';
export type PropertiesConnectionLabelKey = 'fragmentConcurrency' | 'torrentConnectedPeers' | 'connections';
@@ -12,6 +13,12 @@ export type PropertiesConnectionPresentation = {
const displayCount = (value: number | undefined): string => value == null ? '—' : String(value);
export const getPropertiesProgress = (
snapshot: Pick<PropertiesSnapshot, 'status' | 'moveProgress' | 'fraction' | 'downloadedBytes' | 'totalBytes' | 'totalIsEstimate' | 'isMedia' | 'size'>,
): number => snapshot.status === 'moving'
? Math.max(0, Math.min(1, snapshot.moveProgress ?? 0))
: resolveDownloadFraction(snapshot);
export const getPropertiesConnectionPresentation = (
snapshot: Pick<PropertiesSnapshot, 'isMedia' | 'isTorrent' | 'connections' | 'activeConnections' | 'requestedConnections' | 'connectedPeers'>,
): PropertiesConnectionPresentation => {