Release v1.5.87

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-03-04 02:38:05 +01:00
co-authored by Claude Opus 4.6
parent d63afcce89
commit 7af9d67770
7 changed files with 635 additions and 239 deletions
+7 -1
View File
@@ -16,6 +16,10 @@ export const DLC_AES_IV = Buffer.from("9bc24cb995cb8db3", "utf8");
export const REQUEST_RETRIES = 3;
export const CHUNK_SIZE = 512 * 1024;
export const WRITE_BUFFER_SIZE = 512 * 1024; // 512 KB write buffer (JDownloader: 500 KB)
export const WRITE_FLUSH_TIMEOUT_MS = 2000; // 2s flush timeout
export const ALLOCATION_UNIT_SIZE = 4096; // 4 KB NTFS alignment
export const SAMPLE_DIR_NAMES = new Set(["sample", "samples"]);
export const SAMPLE_VIDEO_EXTENSIONS = new Set([".mkv", ".mp4", ".avi", ".mov", ".wmv", ".m4v", ".ts", ".m2ts", ".webm"]);
export const LINK_ARTIFACT_EXTENSIONS = new Set([".url", ".webloc", ".dlc", ".rsdf", ".ccf"]);
@@ -78,6 +82,8 @@ export function defaultSettings(): AppSettings {
autoSkipExtracted: false,
confirmDeleteSelection: true,
totalDownloadedAllTime: 0,
bandwidthSchedules: []
bandwidthSchedules: [],
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
extractCpuPriority: "high"
};
}
+193 -14
View File
@@ -19,7 +19,7 @@ import {
StartConflictResolutionResult,
UiSnapshot
} from "../shared/types";
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS } from "./constants";
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE } from "./constants";
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
import { collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
@@ -267,6 +267,14 @@ function isExtractedLabel(statusText: string): boolean {
return /^entpackt\b/i.test(String(statusText || "").trim());
}
function formatExtractDone(elapsedMs: number): string {
if (elapsedMs < 1000) return "Entpackt - Done (<1s)";
const secs = elapsedMs / 1000;
return secs < 100
? `Entpackt - Done (${secs.toFixed(1)}s)`
: `Entpackt - Done (${Math.round(secs)}s)`;
}
function providerLabel(provider: DownloadItem["provider"]): string {
if (provider === "realdebrid") {
return "Real-Debrid";
@@ -2432,12 +2440,89 @@ export class DownloadManager extends EventEmitter {
this.emitState(true);
}
public resetItems(itemIds: string[]): void {
const affectedPackageIds = new Set<string>();
for (const itemId of itemIds) {
const item = this.session.items[itemId];
if (!item) continue;
affectedPackageIds.add(item.packageId);
const active = this.activeTasks.get(itemId);
if (active) {
active.abortReason = "cancel";
active.abortController.abort("cancel");
}
const targetPath = String(item.targetPath || "").trim();
if (targetPath) {
try { fs.rmSync(targetPath, { force: true }); } catch { /* ignore */ }
this.releaseTargetPath(itemId);
}
this.dropItemContribution(itemId);
this.runOutcomes.delete(itemId);
this.runItemIds.delete(itemId);
this.retryAfterByItem.delete(itemId);
this.retryStateByItem.delete(itemId);
item.status = "queued";
item.downloadedBytes = 0;
item.totalBytes = null;
item.progressPercent = 0;
item.speedBps = 0;
item.attempts = 0;
item.retries = 0;
item.lastError = "";
item.resumable = true;
item.targetPath = "";
item.provider = null;
item.fullStatus = "Wartet";
item.updatedAt = nowMs();
}
// Reset parent package status if it was completed/failed (now has queued items again)
for (const pkgId of affectedPackageIds) {
const pkg = this.session.packages[pkgId];
if (pkg && (pkg.status === "completed" || pkg.status === "failed" || pkg.status === "cancelled")) {
pkg.status = "queued";
pkg.cancelled = false;
pkg.updatedAt = nowMs();
}
}
logger.info(`${itemIds.length} Item(s) zurückgesetzt`);
this.persistSoon();
this.emitState(true);
}
public setPackagePriority(packageId: string, priority: PackagePriority): void {
const pkg = this.session.packages[packageId];
if (!pkg) return;
if (priority !== "high" && priority !== "normal" && priority !== "low") return;
pkg.priority = priority;
pkg.updatedAt = nowMs();
// Move high-priority packages to the top of packageOrder
if (priority === "high") {
const order = this.session.packageOrder;
const idx = order.indexOf(packageId);
if (idx > 0) {
order.splice(idx, 1);
// Insert after last existing high-priority package
let insertAt = 0;
for (let i = 0; i < order.length; i++) {
const p = this.session.packages[order[i]];
if (p && p.priority === "high") {
insertAt = i + 1;
} else {
break;
}
}
order.splice(insertAt, 0, packageId);
}
}
this.persistSoon();
this.emitState();
}
@@ -4601,7 +4686,22 @@ export class DownloadManager extends EventEmitter {
}
await fs.promises.mkdir(path.dirname(effectiveTargetPath), { recursive: true });
const stream = fs.createWriteStream(effectiveTargetPath, { flags: writeMode });
// Sparse file pre-allocation (Windows only, new files with known size)
let preAllocated = false;
if (writeMode === "w" && item.totalBytes && item.totalBytes > 0 && process.platform === "win32") {
try {
const fd = await fs.promises.open(effectiveTargetPath, "w");
await fd.truncate(item.totalBytes);
await fd.close();
preAllocated = true;
} catch { /* best-effort */ }
}
const stream = fs.createWriteStream(effectiveTargetPath, {
flags: preAllocated ? "r+" : writeMode,
start: preAllocated ? 0 : undefined
});
let written = writeMode === "a" ? existingBytes : 0;
let windowBytes = 0;
let windowStarted = nowMs();
@@ -4694,6 +4794,28 @@ export class DownloadManager extends EventEmitter {
active.abortController.signal.addEventListener("abort", onAbort, { once: true });
});
// Write-buffer with 4KB NTFS alignment (JDownloader-style)
const writeBuf = Buffer.allocUnsafe(WRITE_BUFFER_SIZE);
let writeBufPos = 0;
let lastFlushAt = nowMs();
const alignedFlush = async (final = false): Promise<void> => {
if (writeBufPos === 0) return;
let toWrite = writeBufPos;
if (!final && toWrite > ALLOCATION_UNIT_SIZE) {
toWrite = toWrite - (toWrite % ALLOCATION_UNIT_SIZE);
}
const slice = Buffer.from(writeBuf.subarray(0, toWrite));
if (!stream.write(slice)) {
await waitDrain();
}
if (toWrite < writeBufPos) {
writeBuf.copy(writeBuf, 0, toWrite, writeBufPos);
}
writeBufPos -= toWrite;
lastFlushAt = nowMs();
};
let bodyError: unknown = null;
try {
const body = response.body;
@@ -4814,9 +4936,24 @@ export class DownloadManager extends EventEmitter {
if (active.abortController.signal.aborted) {
throw new Error(`aborted:${active.abortReason}`);
}
if (!stream.write(buffer)) {
await waitDrain();
// Buffer incoming data for aligned writes
let srcOffset = 0;
while (srcOffset < buffer.length) {
const space = WRITE_BUFFER_SIZE - writeBufPos;
const toCopy = Math.min(space, buffer.length - srcOffset);
buffer.copy(writeBuf, writeBufPos, srcOffset, srcOffset + toCopy);
writeBufPos += toCopy;
srcOffset += toCopy;
if (writeBufPos >= Math.floor(WRITE_BUFFER_SIZE * 0.80)) {
await alignedFlush(false);
}
}
// Time-based flush
if (writeBufPos > 0 && nowMs() - lastFlushAt >= WRITE_FLUSH_TIMEOUT_MS) {
await alignedFlush(false);
}
written += buffer.length;
windowBytes += buffer.length;
this.session.totalDownloadedBytes += buffer.length;
@@ -4868,6 +5005,14 @@ export class DownloadManager extends EventEmitter {
bodyError = error;
throw error;
} finally {
// Flush remaining buffered data before closing stream
try {
await alignedFlush(true);
} catch (flushError) {
if (!bodyError) {
bodyError = flushError;
}
}
try {
await new Promise<void>((resolve, reject) => {
if (stream.closed || stream.destroyed) {
@@ -4920,6 +5065,14 @@ export class DownloadManager extends EventEmitter {
throw new Error(`Download zu klein (${written} B) Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
}
// Truncate pre-allocated files to actual bytes written to prevent zero-padded tail
if (preAllocated && item.totalBytes && written < item.totalBytes) {
try {
await fs.promises.truncate(effectiveTargetPath, written);
} catch { /* best-effort */ }
logger.warn(`Pre-alloc underflow: erwartet=${item.totalBytes}, erhalten=${written} für ${item.fileName}`);
}
item.downloadedBytes = written;
item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 100;
item.speedBps = 0;
@@ -5434,6 +5587,7 @@ export class DownloadManager extends EventEmitter {
// Track multiple active archives for parallel hybrid extraction
const activeHybridArchiveMap = new Map<string, DownloadItem[]>();
const hybridArchiveStartTimes = new Map<string, number>();
let hybridLastEmitAt = 0;
// Mark hybrid items as pending, others as waiting for parts
@@ -5470,19 +5624,23 @@ export class DownloadManager extends EventEmitter {
packageId,
hybridMode: true,
maxParallel: this.settings.maxParallelExtract || 2,
extractCpuPriority: this.settings.extractCpuPriority,
onProgress: (progress) => {
if (progress.phase === "done") {
// Mark all remaining active archives as done
for (const [, archItems] of activeHybridArchiveMap) {
for (const [archName, archItems] of activeHybridArchiveMap) {
const doneAt = nowMs();
const startedAt = hybridArchiveStartTimes.get(archName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of archItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = "Entpackt - Done";
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
}
activeHybridArchiveMap.clear();
hybridArchiveStartTimes.clear();
return;
}
@@ -5490,19 +5648,23 @@ export class DownloadManager extends EventEmitter {
// Resolve items for this archive if not yet tracked
if (!activeHybridArchiveMap.has(progress.archiveName)) {
activeHybridArchiveMap.set(progress.archiveName, resolveArchiveItems(progress.archiveName));
hybridArchiveStartTimes.set(progress.archiveName, nowMs());
}
const archItems = activeHybridArchiveMap.get(progress.archiveName)!;
// If archive is at 100%, mark its items as done and remove from active
if (Number(progress.archivePercent ?? 0) >= 100) {
const doneAt = nowMs();
const startedAt = hybridArchiveStartTimes.get(progress.archiveName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of archItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = "Entpackt - Done";
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
activeHybridArchiveMap.delete(progress.archiveName);
hybridArchiveStartTimes.delete(progress.archiveName);
} else {
// Update this archive's items with current progress
const archive = ` · ${progress.archiveName}`;
@@ -5704,6 +5866,7 @@ export class DownloadManager extends EventEmitter {
try {
// Track multiple active archives for parallel extraction
const activeArchiveItemsMap = new Map<string, DownloadItem[]>();
const archiveStartTimes = new Map<string, number>();
const result = await extractPackageArchives({
packageDir: pkg.outputDir,
@@ -5716,19 +5879,23 @@ export class DownloadManager extends EventEmitter {
signal: extractAbortController.signal,
packageId,
maxParallel: this.settings.maxParallelExtract || 2,
extractCpuPriority: this.settings.extractCpuPriority,
onProgress: (progress) => {
if (progress.phase === "done") {
// Mark all remaining active archives as done
for (const [, items] of activeArchiveItemsMap) {
for (const [archName, items] of activeArchiveItemsMap) {
const doneAt = nowMs();
const startedAt = archiveStartTimes.get(archName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of items) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = "Entpackt - Done";
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
}
activeArchiveItemsMap.clear();
archiveStartTimes.clear();
emitExtractStatus("Entpacken 100%", true);
return;
}
@@ -5737,19 +5904,23 @@ export class DownloadManager extends EventEmitter {
// Resolve items for this archive if not yet tracked
if (!activeArchiveItemsMap.has(progress.archiveName)) {
activeArchiveItemsMap.set(progress.archiveName, resolveArchiveItems(progress.archiveName));
archiveStartTimes.set(progress.archiveName, nowMs());
}
const archiveItems = activeArchiveItemsMap.get(progress.archiveName)!;
// If archive is at 100%, mark its items as done and remove from active
if (Number(progress.archivePercent ?? 0) >= 100) {
const doneAt = nowMs();
const startedAt = archiveStartTimes.get(progress.archiveName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of archiveItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = "Entpackt - Done";
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
activeArchiveItemsMap.delete(progress.archiveName);
archiveStartTimes.delete(progress.archiveName);
} else {
// Update this archive's items with current progress
const archive = progress.archiveName ? ` · ${progress.archiveName}` : "";
@@ -5799,9 +5970,13 @@ export class DownloadManager extends EventEmitter {
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
if (result.failed > 0) {
const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen");
const failAt = nowMs();
for (const entry of completedItems) {
entry.fullStatus = `Entpack-Fehler: ${reason}`;
entry.updatedAt = nowMs();
// Preserve per-archive "Entpackt - Done (X.Xs)" labels for successfully extracted archives
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = `Entpack-Fehler: ${reason}`;
}
entry.updatedAt = failAt;
}
pkg.status = "failed";
} else {
@@ -5821,9 +5996,13 @@ export class DownloadManager extends EventEmitter {
finalStatusText = "Entpackt (keine Archive)";
}
const finalAt = nowMs();
for (const entry of completedItems) {
entry.fullStatus = finalStatusText;
entry.updatedAt = nowMs();
// Preserve per-archive duration labels (e.g. "Entpackt - Done (5.3s)")
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = finalStatusText;
}
entry.updatedAt = finalAt;
}
pkg.status = "completed";
}
+30 -12
View File
@@ -72,6 +72,7 @@ const EXTRACTOR_RETRY_AFTER_MS = 30_000;
const DEFAULT_ZIP_ENTRY_MEMORY_LIMIT_MB = 256;
const EXTRACTOR_PROBE_TIMEOUT_MS = 8_000;
const DEFAULT_EXTRACT_CPU_BUDGET_PERCENT = 80;
let currentExtractCpuPriority: string | undefined;
export interface ExtractOptions {
packageDir: string;
@@ -88,6 +89,7 @@ export interface ExtractOptions {
packageId?: string;
hybridMode?: boolean;
maxParallel?: number;
extractCpuPriority?: string;
}
export interface ExtractProgressUpdate {
@@ -566,15 +568,30 @@ function shouldUseExtractorPerformanceFlags(): boolean {
return raw !== "0" && raw !== "false" && raw !== "off" && raw !== "no";
}
function extractCpuBudgetPercent(): number {
function extractCpuBudgetFromPriority(priority?: string): number {
switch (priority) {
case "low": return 25;
case "middle": return 50;
default: return 80;
}
}
function extractOsPriority(priority?: string): number {
switch (priority) {
case "high": return os.constants.priority.PRIORITY_BELOW_NORMAL;
default: return os.constants.priority.PRIORITY_LOW;
}
}
function extractCpuBudgetPercent(priority?: string): number {
const envValue = Number(process.env.RD_EXTRACT_CPU_BUDGET_PERCENT ?? NaN);
if (Number.isFinite(envValue) && envValue >= 40 && envValue <= 95) {
return Math.floor(envValue);
}
return DEFAULT_EXTRACT_CPU_BUDGET_PERCENT;
return extractCpuBudgetFromPriority(priority);
}
function extractorThreadSwitch(hybridMode = false): string {
function extractorThreadSwitch(hybridMode = false, priority?: string): string {
if (hybridMode) {
// 2 threads during hybrid extraction (download + extract simultaneously).
// JDownloader 2 uses in-process 7-Zip-JBinding which naturally limits throughput
@@ -586,13 +603,13 @@ function extractorThreadSwitch(hybridMode = false): string {
return `-mt${Math.floor(envValue)}`;
}
const cpuCount = Math.max(1, os.cpus().length || 1);
const budgetPercent = extractCpuBudgetPercent();
const budgetPercent = extractCpuBudgetPercent(priority);
const budgetedThreads = Math.floor((cpuCount * budgetPercent) / 100);
const threadCount = Math.max(1, Math.min(16, Math.max(1, budgetedThreads)));
return `-mt${threadCount}`;
}
function lowerExtractProcessPriority(childPid: number | undefined): void {
function lowerExtractProcessPriority(childPid: number | undefined, cpuPriority?: string): void {
if (process.platform !== "win32") {
return;
}
@@ -601,9 +618,9 @@ function lowerExtractProcessPriority(childPid: number | undefined): void {
return;
}
try {
// IDLE_PRIORITY_CLASS: lowers CPU scheduling priority so extraction
// doesn't starve other processes. I/O priority stays Normal (like JDownloader 2).
os.setPriority(pid, os.constants.priority.PRIORITY_LOW);
// Lowers CPU scheduling priority so extraction doesn't starve other processes.
// high → BELOW_NORMAL, middle/low → IDLE. I/O priority stays Normal (like JDownloader 2).
os.setPriority(pid, extractOsPriority(cpuPriority));
} catch {
// ignore: priority lowering is best-effort
}
@@ -673,7 +690,7 @@ function runExtractCommand(
let settled = false;
let output = "";
const child = spawn(command, args, { windowsHide: true });
lowerExtractProcessPriority(child.pid);
lowerExtractProcessPriority(child.pid, currentExtractCpuPriority);
let timeoutId: NodeJS.Timeout | null = null;
let timedOutByWatchdog = false;
let abortedBySignal = false;
@@ -995,7 +1012,7 @@ function runJvmExtractCommand(
let stderrBuffer = "";
const child = spawn(layout.javaCommand, args, { windowsHide: true });
lowerExtractProcessPriority(child.pid);
lowerExtractProcessPriority(child.pid, currentExtractCpuPriority);
const flushLines = (rawChunk: string, fromStdErr = false): void => {
if (!rawChunk) {
@@ -1174,7 +1191,7 @@ export function buildExternalExtractArgs(
// On Windows (the target platform) this is less of a concern than on shared Unix systems.
const pass = password ? `-p${password}` : "-p-";
const perfArgs = usePerformanceFlags && shouldUseExtractorPerformanceFlags()
? ["-idc", extractorThreadSwitch(hybridMode)]
? ["-idc", extractorThreadSwitch(hybridMode, currentExtractCpuPriority)]
: [];
return ["x", overwrite, pass, "-y", ...perfArgs, archivePath, `${targetDir}${path.sep}`];
}
@@ -1824,7 +1841,6 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
if (options.signal?.aborted) {
throw new Error("aborted:extract");
}
const allCandidates = await findArchiveCandidates(options.packageDir);
const candidates = options.onlyArchives
? allCandidates.filter((archivePath) => {
@@ -1972,6 +1988,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
? (attempt: number, total: number) => { emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordAttempt: attempt, passwordTotal: total }); }
: undefined;
try {
// Set module-level priority before each extract call (race-safe: spawn is synchronous)
currentExtractCpuPriority = options.extractCpuPriority;
const ext = path.extname(archivePath).toLowerCase();
if (ext === ".zip") {
const preferExternal = await shouldPreferExternalZip(archivePath);
+33 -2
View File
@@ -1,7 +1,7 @@
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, PackageEntry, SessionState } from "../shared/types";
import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { defaultSettings } from "./constants";
import { logger } from "./logger";
@@ -12,6 +12,8 @@ const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
const VALID_SPEED_MODES = new Set(["global", "per_download"]);
const VALID_THEMES = new Set(["dark", "light"]);
const VALID_EXTRACT_CPU_PRIORITIES = new Set(["high", "middle", "low"]);
const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
]);
@@ -65,6 +67,29 @@ function normalizeAbsoluteDir(value: unknown, fallback: string): string {
return path.resolve(text);
}
const DEFAULT_COLUMN_ORDER = ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"];
const ALL_VALID_COLUMNS = new Set([...DEFAULT_COLUMN_ORDER, "added"]);
function normalizeColumnOrder(raw: unknown): string[] {
if (!Array.isArray(raw) || raw.length === 0) {
return [...DEFAULT_COLUMN_ORDER];
}
const valid = ALL_VALID_COLUMNS;
const seen = new Set<string>();
const result: string[] = [];
for (const col of raw) {
if (typeof col === "string" && valid.has(col) && !seen.has(col)) {
seen.add(col);
result.push(col);
}
}
// "name" is mandatory — ensure it's always present
if (!seen.has("name")) {
result.unshift("name");
}
return result;
}
export function normalizeSettings(settings: AppSettings): AppSettings {
const defaults = defaultSettings();
const normalized: AppSettings = {
@@ -112,7 +137,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime,
theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme,
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules)
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules),
columnOrder: normalizeColumnOrder(settings.columnOrder),
extractCpuPriority: settings.extractCpuPriority
};
if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) {
@@ -142,6 +169,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
if (!VALID_SPEED_MODES.has(normalized.speedLimitMode)) {
normalized.speedLimitMode = defaults.speedLimitMode;
}
if (!VALID_EXTRACT_CPU_PRIORITIES.has(normalized.extractCpuPriority)) {
normalized.extractCpuPriority = defaults.extractCpuPriority;
}
return normalized;
}
@@ -274,6 +304,7 @@ function normalizeLoadedSession(raw: unknown): SessionState {
.filter((value) => value.length > 0),
cancelled: Boolean(pkg.cancelled),
enabled: pkg.enabled === undefined ? true : Boolean(pkg.enabled),
priority: VALID_PACKAGE_PRIORITIES.has(asText(pkg.priority)) ? asText(pkg.priority) as PackagePriority : "normal",
createdAt: clampNumber(pkg.createdAt, now, 0, Number.MAX_SAFE_INTEGER),
updatedAt: clampNumber(pkg.updatedAt, now, 0, Number.MAX_SAFE_INTEGER)
};