Compare commits

..

No commits in common. "04413599d8df41d744d2107c79c13f65a1677767" and "ca477733172df704ceca0171d3f51d1a1c75250b" have entirely different histories.

4 changed files with 31 additions and 181 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.7.139",
"version": "1.7.138",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -1578,17 +1578,6 @@ export class DownloadManager extends EventEmitter {
this.settingsSnapshotCacheAt = 0;
}
/** State-diffing tracking: hashes of items/packages as last sent to the
* renderer. Allows getSnapshotForEmit() to return only changed entries
* plus a list of removed IDs, drastically cutting IPC payload size for
* large queues (5000+ items) where most items are idle between emits. */
private lastEmittedItemHashes = new Map<string, string>();
private lastEmittedPackageHashes = new Map<string, string>();
private firstEmitDone = false;
private lastFullEmitAt = 0;
/** Force a full resync every 30 seconds to recover from any potential drift. */
private static readonly FULL_RESYNC_INTERVAL_MS = 30000;
private lastPersistAt = 0;
private lastSettingsPersistAt = 0;
private appSessionStartedAt = 0;
@ -2030,104 +2019,6 @@ export class DownloadManager extends EventEmitter {
this.emitState();
}
/** Compact hash of the visible/mutable item fields. Two items with identical
* hashes are considered "no visible change" and can be excluded from delta
* emits. Field selection covers everything ItemRow/PackageCard render. */
private buildItemHash(item: DownloadItem): string {
return `${item.updatedAt}|${item.status}|${item.progressPercent}|${item.speedBps}|${item.downloadedBytes}|${item.totalBytes}|${item.retries}|${item.fullStatus || ""}|${item.fileName}|${item.providerLabel || ""}|${item.provider || ""}|${item.onlineStatus || ""}|${item.lastError || ""}`;
}
/** Compact hash of the visible/mutable package fields. */
private buildPackageHash(pkg: PackageEntry): string {
return `${pkg.updatedAt}|${pkg.status}|${pkg.name}|${pkg.enabled ? 1 : 0}|${pkg.cancelled ? 1 : 0}|${pkg.priority || ""}|${pkg.itemIds.length}|${pkg.postProcessLabel || ""}`;
}
/** Returns a snapshot suitable for IPC emit. On the first emit (or every
* 30s for safety, or when explicitly forced), returns a "full" payload
* containing all items/packages. Otherwise returns a "delta" with only
* items/packages that changed since the last emit, plus removed IDs. */
public getSnapshotForEmit(forceFull = false): UiSnapshot {
const base = this.getSnapshot();
const now = nowMs();
const needsFullResync = !this.firstEmitDone || forceFull
|| (now - this.lastFullEmitAt) > DownloadManager.FULL_RESYNC_INTERVAL_MS;
if (needsFullResync) {
// Refresh tracking state to current snapshot
this.lastEmittedItemHashes.clear();
this.lastEmittedPackageHashes.clear();
for (const id in base.session.items) {
this.lastEmittedItemHashes.set(id, this.buildItemHash(base.session.items[id]));
}
for (const id in base.session.packages) {
this.lastEmittedPackageHashes.set(id, this.buildPackageHash(base.session.packages[id]));
}
this.firstEmitDone = true;
this.lastFullEmitAt = now;
return { ...base, payloadKind: "full" };
}
// Build deltas: include only items/packages whose hash changed since last emit
const changedItems: Record<string, DownloadItem> = {};
const removedItemIds: string[] = [];
const seenItemIds = new Set<string>();
let itemChangeCount = 0;
for (const id in base.session.items) {
seenItemIds.add(id);
const item = base.session.items[id];
const newHash = this.buildItemHash(item);
const oldHash = this.lastEmittedItemHashes.get(id);
if (oldHash !== newHash) {
changedItems[id] = item;
this.lastEmittedItemHashes.set(id, newHash);
itemChangeCount += 1;
}
}
// Detect removed items
for (const id of this.lastEmittedItemHashes.keys()) {
if (!seenItemIds.has(id)) {
removedItemIds.push(id);
}
}
for (const id of removedItemIds) {
this.lastEmittedItemHashes.delete(id);
}
const changedPackages: Record<string, PackageEntry> = {};
const removedPackageIds: string[] = [];
const seenPackageIds = new Set<string>();
for (const id in base.session.packages) {
seenPackageIds.add(id);
const pkg = base.session.packages[id];
const newHash = this.buildPackageHash(pkg);
const oldHash = this.lastEmittedPackageHashes.get(id);
if (oldHash !== newHash) {
changedPackages[id] = pkg;
this.lastEmittedPackageHashes.set(id, newHash);
}
}
for (const id of this.lastEmittedPackageHashes.keys()) {
if (!seenPackageIds.has(id)) {
removedPackageIds.push(id);
}
}
for (const id of removedPackageIds) {
this.lastEmittedPackageHashes.delete(id);
}
return {
...base,
session: {
...base.session,
items: changedItems,
packages: changedPackages,
},
payloadKind: "delta",
removedItemIds,
removedPackageIds,
};
}
public getSnapshot(): UiSnapshot {
const now = nowMs();
this.ensureProviderDailyUsageFresh(now, true);
@ -5389,7 +5280,7 @@ export class DownloadManager extends EventEmitter {
this.stateEmitTimer = null;
}
this.lastStateEmitAt = now;
this.emit("state", this.getSnapshotForEmit());
this.emit("state", this.getSnapshot());
return;
}
// Too soon — replace any pending timer with a shorter forced-emit timer
@ -5400,7 +5291,7 @@ export class DownloadManager extends EventEmitter {
this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null;
this.lastStateEmitAt = nowMs();
this.emit("state", this.getSnapshotForEmit());
this.emit("state", this.getSnapshot());
}, MIN_FORCE_GAP_MS - sinceLastEmit);
return;
}
@ -5420,7 +5311,7 @@ export class DownloadManager extends EventEmitter {
this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null;
this.lastStateEmitAt = nowMs();
this.emit("state", this.getSnapshotForEmit());
this.emit("state", this.getSnapshot());
}, emitDelay);
}

View File

@ -1532,11 +1532,6 @@ export function App(): ReactElement {
const settingsDraftRevisionRef = useRef(0);
const panelDirtyRevisionRef = useRef(0);
const latestStateRef = useRef<UiSnapshot | null>(null);
// Master state used to apply incoming delta payloads. The wire format from
// the main process sends only changed items/packages (with payloadKind="delta")
// most of the time and a full snapshot every 30s for safety. Without this
// master, we'd only see the changed slice each emit.
const masterSnapshotRef = useRef<UiSnapshot | null>(null);
const snapshotRef = useRef(snapshot);
snapshotRef.current = snapshot;
const tabRef = useRef(tab);
@ -1868,8 +1863,6 @@ export function App(): ReactElement {
if (!mountedRef.current) {
return;
}
// Seed the master snapshot — incoming delta payloads will merge into this.
masterSnapshotRef.current = state;
setSnapshot(state);
if (state.settings.columnOrder?.length > 0) {
setColumnOrder(state.settings.columnOrder);
@ -1890,36 +1883,11 @@ export function App(): ReactElement {
}).catch((error) => {
showToast(`Snapshot konnte nicht geladen werden: ${String(error)}`, 2800);
});
unsubscribe = window.rd.onStateUpdate((wireState) => {
// Merge delta payloads into the master snapshot. Full payloads replace
// the master entirely (initial sync + periodic 30s resync).
let merged: UiSnapshot;
const master = masterSnapshotRef.current;
if (wireState.payloadKind === "delta" && master) {
const newItems: Record<string, DownloadItem> = { ...master.session.items, ...wireState.session.items };
if (wireState.removedItemIds && wireState.removedItemIds.length > 0) {
for (const id of wireState.removedItemIds) delete newItems[id];
}
const newPackages: Record<string, PackageEntry> = { ...master.session.packages, ...wireState.session.packages };
if (wireState.removedPackageIds && wireState.removedPackageIds.length > 0) {
for (const id of wireState.removedPackageIds) delete newPackages[id];
}
merged = {
...wireState,
session: {
...wireState.session,
items: newItems,
packages: newPackages,
},
};
} else {
merged = wireState;
}
masterSnapshotRef.current = merged;
latestStateRef.current = merged;
unsubscribe = window.rd.onStateUpdate((state) => {
latestStateRef.current = state;
if (stateFlushTimerRef.current) { return; }
const itemCount = Object.keys(merged.session.items).length;
const itemCount = Object.keys(state.session.items).length;
let flushDelay = itemCount >= 1500
? 900
: itemCount >= 700
@ -1927,7 +1895,7 @@ export function App(): ReactElement {
: itemCount >= 250
? 400
: 150;
if (!merged.session.running) {
if (!state.session.running) {
flushDelay = Math.min(flushDelay, 200);
}
if (activeTabRef.current !== "downloads") {

View File

@ -230,15 +230,6 @@ export interface UiSnapshot {
clipboardActive: boolean;
reconnectSeconds: number;
packageSpeedBps: Record<string, number>;
/** When set to "delta", session.items contains ONLY items that changed since
* the last emit, and removedItemIds lists items that were removed. The
* renderer must merge these into its master state. When undefined or "full",
* session.items is the complete set (initial sync or periodic resync). */
payloadKind?: "full" | "delta";
/** Item IDs to remove from the renderer's master state when payloadKind="delta". */
removedItemIds?: string[];
/** Package IDs to remove from the renderer's master state when payloadKind="delta". */
removedPackageIds?: string[];
}
export interface AddLinksPayload {
@ -303,10 +294,10 @@ export interface UpdateInstallProgress {
message: string;
}
export type AllDebridHostState = "up" | "down" | "not_tracked" | "unknown";
export type AllDebridHostInfoSource = "api" | "web";
export type DebridLinkHostState = "up" | "down" | "unknown";
export type DebridLinkKeyState = "ready" | "cooldown" | "invalid" | "quota" | "rate_limit" | "error" | "unknown";
export type AllDebridHostState = "up" | "down" | "not_tracked" | "unknown";
export type AllDebridHostInfoSource = "api" | "web";
export type DebridLinkHostState = "up" | "down" | "unknown";
export type DebridLinkKeyState = "ready" | "cooldown" | "invalid" | "quota" | "rate_limit" | "error" | "unknown";
export interface AllDebridHostInfo {
host: string;
@ -322,26 +313,26 @@ export interface AllDebridHostInfo {
note: string;
}
export interface DebridLinkHostLimitInfo {
keyId: string;
keyLabel: string;
host: string;
fetchedAt: number;
export interface DebridLinkHostLimitInfo {
keyId: string;
keyLabel: string;
host: string;
fetchedAt: number;
trafficCurrentBytes: number | null;
trafficMaxBytes: number | null;
linksCurrent: number | null;
linksMax: number | null;
note: string;
state: DebridLinkKeyState;
stateLabel: string;
stateDetail: string;
cooldownUntil: number | null;
cooldownRemainingMs: number;
lastCheckedAt: number | null;
hostState: DebridLinkHostState;
hostStateLabel: string;
hostNote: string;
}
trafficMaxBytes: number | null;
linksCurrent: number | null;
linksMax: number | null;
note: string;
state: DebridLinkKeyState;
stateLabel: string;
stateDetail: string;
cooldownUntil: number | null;
cooldownRemainingMs: number;
lastCheckedAt: number | null;
hostState: DebridLinkHostState;
hostStateLabel: string;
hostNote: string;
}
export interface ParsedHashEntry {
fileName: string;