mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-10 11:37:21 +00:00
fix(downloads): harden queue and lifecycle synchronization
Serialize queue controls, make multi-item moves atomic, await stale enqueue cleanup, and guard late media and progress events. Add deterministic table sorting and regression coverage for worst-case lifecycle races.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import type { DownloadItem } from '../bindings/DownloadItem';
|
||||
|
||||
export type DownloadSortColumn = 'File Name' | 'Size' | 'Status' | 'Speed' | 'ETA' | 'Date Added';
|
||||
export type DownloadSortDirection = 'asc' | 'desc';
|
||||
|
||||
export type DownloadSortConfig = {
|
||||
column: DownloadSortColumn;
|
||||
direction: DownloadSortDirection;
|
||||
};
|
||||
|
||||
const SIZE_UNITS: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
KIB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
MIB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
GIB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
TIB: 1024 ** 4,
|
||||
};
|
||||
|
||||
const valueOrNull = (value?: string): string | null => {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed && trimmed !== '-' && !/^unknown$/i.test(trimmed) ? trimmed : null;
|
||||
};
|
||||
|
||||
const parseUnitValue = (value?: string, units = SIZE_UNITS): number | null => {
|
||||
const normalized = valueOrNull(value);
|
||||
if (!normalized) return null;
|
||||
const match = normalized.match(/^([\d.,]+)\s*([KMGT]?I?B)(?:\/s)?$/i);
|
||||
if (!match) {
|
||||
const number = Number(normalized.replace(/,/g, ''));
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
const amount = Number(match[1].replace(/,/g, ''));
|
||||
const multiplier = units[match[2].toUpperCase()];
|
||||
return Number.isFinite(amount) && multiplier ? amount * multiplier : null;
|
||||
};
|
||||
|
||||
export const parseDownloadSize = (value?: string): number | null => parseUnitValue(value);
|
||||
|
||||
export const parseDownloadSpeed = (value?: string): number | null =>
|
||||
parseUnitValue(value, SIZE_UNITS);
|
||||
|
||||
export const parseDownloadEta = (value?: string): number | null => {
|
||||
const normalized = valueOrNull(value);
|
||||
if (!normalized) return null;
|
||||
|
||||
const clockParts = normalized.split(':').map(part => Number(part));
|
||||
if (clockParts.length >= 2 && clockParts.every(Number.isFinite)) {
|
||||
return clockParts.reduce((total, part, index) => total + part * 60 ** (clockParts.length - index - 1), 0);
|
||||
}
|
||||
|
||||
let seconds = 0;
|
||||
let matched = false;
|
||||
for (const [pattern, multiplier] of [[/(\d+(?:\.\d+)?)h/i, 3600], [/(\d+(?:\.\d+)?)m/i, 60], [/(\d+(?:\.\d+)?)s/i, 1]] as const) {
|
||||
const match = normalized.match(pattern);
|
||||
if (match) {
|
||||
seconds += Number(match[1]) * multiplier;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
return matched && Number.isFinite(seconds) ? seconds : null;
|
||||
};
|
||||
|
||||
const compareValues = (left: string | number | null, right: string | number | null): number => {
|
||||
if (left === null && right === null) return 0;
|
||||
if (left === null) return 1;
|
||||
if (right === null) return -1;
|
||||
if (typeof left === 'number' && typeof right === 'number') return left - right;
|
||||
return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
|
||||
};
|
||||
|
||||
const parseDownloadDate = (value?: string): number | null => {
|
||||
if (!value?.trim()) return null;
|
||||
const timestamp = new Date(value).getTime();
|
||||
return Number.isFinite(timestamp) ? timestamp : null;
|
||||
};
|
||||
|
||||
export const sortDownloads = (downloads: DownloadItem[], config: DownloadSortConfig): DownloadItem[] => {
|
||||
const sorted = [...downloads].sort((left, right) => {
|
||||
let comparison: number;
|
||||
switch (config.column) {
|
||||
case 'File Name':
|
||||
comparison = compareValues(left.fileName || left.url, right.fileName || right.url);
|
||||
break;
|
||||
case 'Size':
|
||||
comparison = compareValues(parseDownloadSize(left.size), parseDownloadSize(right.size));
|
||||
break;
|
||||
case 'Status':
|
||||
comparison = compareValues(left.status, right.status);
|
||||
break;
|
||||
case 'Speed':
|
||||
comparison = compareValues(parseDownloadSpeed(left.speed), parseDownloadSpeed(right.speed));
|
||||
break;
|
||||
case 'ETA':
|
||||
comparison = compareValues(parseDownloadEta(left.eta), parseDownloadEta(right.eta));
|
||||
break;
|
||||
case 'Date Added':
|
||||
comparison = compareValues(parseDownloadDate(left.dateAdded), parseDownloadDate(right.dateAdded));
|
||||
break;
|
||||
}
|
||||
|
||||
if (comparison === 0) comparison = left.id.localeCompare(right.id);
|
||||
return config.direction === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
return sorted;
|
||||
};
|
||||
Reference in New Issue
Block a user