Prepare v2.0.74 sidepanel reliability release
Restore and persist the package-based link collector with progressive metadata, bounded high-volume updates, safe hydration, visible selection, and complete localization. Harden download controls, snapshot ordering, history pagination, statistics recovery, settings saves, backup imports, notification persistence, and Windows storage races with regression coverage.
This commit is contained in:
+70
-7
@@ -2,6 +2,11 @@ export type CollectorAvailability = "online" | "offline" | "unknown";
|
||||
export type CollectorLinkStatus = "ready" | "offline" | "unknown";
|
||||
export type CollectorPackageNameSource = "explicit" | "inferred";
|
||||
|
||||
export const COLLECTOR_MAX_PACKAGES = 2_000;
|
||||
export const COLLECTOR_MAX_LINKS = 20_000;
|
||||
export const COLLECTOR_MAX_NAME_LENGTH = 1_024;
|
||||
export const COLLECTOR_MAX_PERSISTENCE_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
export interface CollectorLink {
|
||||
id: string;
|
||||
url: string;
|
||||
@@ -52,6 +57,64 @@ export interface CollectorPersistenceState {
|
||||
collapsedPackageIds: string[];
|
||||
}
|
||||
|
||||
export type CollectorCapacityResult = {
|
||||
ok: true;
|
||||
packageCount: number;
|
||||
linkCount: number;
|
||||
} | {
|
||||
ok: false;
|
||||
packageCount: number;
|
||||
linkCount: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type CollectorPersistenceSizeResult = {
|
||||
ok: true;
|
||||
byteCount: number;
|
||||
} | {
|
||||
ok: false;
|
||||
byteCount: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function inspectCollectorCapacity(packages: readonly Pick<CollectorPackage, "links">[]): CollectorCapacityResult {
|
||||
const packageCount = packages.length;
|
||||
let linkCount = 0;
|
||||
for (const pkg of packages) linkCount += pkg.links.length;
|
||||
if (packageCount > COLLECTOR_MAX_PACKAGES) {
|
||||
return {
|
||||
ok: false,
|
||||
packageCount,
|
||||
linkCount,
|
||||
message: `Der Linksammler kann höchstens ${COLLECTOR_MAX_PACKAGES.toLocaleString("de-DE")} Pakete enthalten.`
|
||||
};
|
||||
}
|
||||
if (linkCount > COLLECTOR_MAX_LINKS) {
|
||||
return {
|
||||
ok: false,
|
||||
packageCount,
|
||||
linkCount,
|
||||
message: `Der Linksammler kann höchstens ${COLLECTOR_MAX_LINKS.toLocaleString("de-DE")} Links enthalten.`
|
||||
};
|
||||
}
|
||||
return { ok: true, packageCount, linkCount };
|
||||
}
|
||||
|
||||
export function inspectCollectorPersistenceSize(
|
||||
state: CollectorPersistenceState,
|
||||
maximumBytes = COLLECTOR_MAX_PERSISTENCE_BYTES
|
||||
): CollectorPersistenceSizeResult {
|
||||
const byteCount = new TextEncoder().encode(JSON.stringify({
|
||||
version: 1,
|
||||
packages: state.packages,
|
||||
collapsedPackageIds: state.collapsedPackageIds,
|
||||
updatedAt: Number.MAX_SAFE_INTEGER
|
||||
})).byteLength;
|
||||
return byteCount <= maximumBytes
|
||||
? { ok: true, byteCount }
|
||||
: { ok: false, byteCount, message: "Der Linksammler ist zu groß, um gespeichert zu werden." };
|
||||
}
|
||||
|
||||
function validAddedAt(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
||||
}
|
||||
@@ -107,7 +170,7 @@ function validCollectorLink(value: unknown): value is CollectorLink {
|
||||
&& raw.url.length <= 32767
|
||||
&& /^https?:\/\/[^\s]+$/i.test(raw.url)
|
||||
&& typeof raw.fileName === "string"
|
||||
&& raw.fileName.length <= 1024
|
||||
&& raw.fileName.length <= COLLECTOR_MAX_NAME_LENGTH
|
||||
&& (raw.fileSizeBytes === null || (typeof raw.fileSizeBytes === "number" && Number.isSafeInteger(raw.fileSizeBytes) && raw.fileSizeBytes >= 0))
|
||||
&& typeof raw.hoster === "string"
|
||||
&& raw.hoster.length <= 255
|
||||
@@ -125,7 +188,7 @@ function validCollectorPackage(value: unknown): value is CollectorPackage {
|
||||
&& raw.id.length <= 160
|
||||
&& typeof raw.name === "string"
|
||||
&& raw.name.length > 0
|
||||
&& raw.name.length <= 1024
|
||||
&& raw.name.length <= COLLECTOR_MAX_NAME_LENGTH
|
||||
&& (raw.nameSource === "explicit" || raw.nameSource === "inferred")
|
||||
&& Array.isArray(raw.links)
|
||||
&& raw.links.length > 0
|
||||
@@ -144,9 +207,9 @@ export function validateCollectorEnrichmentRequest(value: unknown): CollectorEnr
|
||||
|| raw.requestId.length > 160
|
||||
|| !Array.isArray(raw.packages)
|
||||
|| raw.packages.length === 0
|
||||
|| raw.packages.length > 2_000
|
||||
|| raw.packages.length > COLLECTOR_MAX_PACKAGES
|
||||
|| raw.packages.some((entry) => !validCollectorPackage(entry))
|
||||
|| raw.packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > 20_000) {
|
||||
|| raw.packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > COLLECTOR_MAX_LINKS) {
|
||||
throw new Error("Linksammler-Anreicherung ist ungültig");
|
||||
}
|
||||
return { requestId: raw.requestId, packages: structuredClone(raw.packages) as CollectorPackage[] };
|
||||
@@ -161,11 +224,11 @@ export function validateCollectorPersistenceState(value: unknown): CollectorPers
|
||||
const collapsedPackageIds = raw.collapsedPackageIds;
|
||||
if (Object.keys(raw).some((key) => key !== "packages" && key !== "collapsedPackageIds")
|
||||
|| !Array.isArray(packages)
|
||||
|| packages.length > 2_000
|
||||
|| packages.length > COLLECTOR_MAX_PACKAGES
|
||||
|| packages.some((entry) => !validCollectorPackage(entry))
|
||||
|| packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > 20_000
|
||||
|| packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > COLLECTOR_MAX_LINKS
|
||||
|| !Array.isArray(collapsedPackageIds)
|
||||
|| collapsedPackageIds.length > 2_000
|
||||
|| collapsedPackageIds.length > COLLECTOR_MAX_PACKAGES
|
||||
|| collapsedPackageIds.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.length > 160)) {
|
||||
throw new Error("Linksammler-Speicherzustand ist ungültig");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const IPC_CHANNELS = {
|
||||
ENRICH_COLLECTOR_PACKAGES: "collector:enrich-packages",
|
||||
COLLECTOR_ENRICHMENT_PROGRESS: "collector:enrichment-progress",
|
||||
GET_COLLECTOR_STATE: "collector:get-state",
|
||||
GET_COLLECTOR_STATE_SYNC: "collector:get-state-sync",
|
||||
SAVE_COLLECTOR_STATE: "collector:save-state",
|
||||
SAVE_COLLECTOR_STATE_SYNC: "collector:save-state-sync",
|
||||
GET_START_CONFLICTS: "queue:get-start-conflicts",
|
||||
|
||||
@@ -91,8 +91,9 @@ export interface ElectronApi {
|
||||
enrichCollectorPackages: (request: CollectorEnrichmentRequest) => Promise<CollectorInspectionResult>;
|
||||
onCollectorEnrichmentProgress: (callback: (progress: CollectorEnrichmentProgress) => void) => () => void;
|
||||
getCollectorState: () => Promise<CollectorPersistenceState>;
|
||||
getCollectorStateSync: () => CollectorPersistenceState | null;
|
||||
saveCollectorState: (state: CollectorPersistenceState) => Promise<CollectorPersistenceState>;
|
||||
saveCollectorStateSync: (state: CollectorPersistenceState) => void;
|
||||
saveCollectorStateSync: (state: CollectorPersistenceState) => boolean;
|
||||
getPathForDroppedFile: (file: File) => string;
|
||||
getStartConflicts: () => Promise<StartConflictEntry[]>;
|
||||
resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise<StartConflictResolutionResult>;
|
||||
|
||||
+11
-1
@@ -28,6 +28,7 @@ export type DebridProvider =
|
||||
| "linksnappy";
|
||||
export type DebridFallbackProvider = DebridProvider | "none";
|
||||
export type AppTheme = "dark" | "light";
|
||||
export type ThemePreference = AppTheme | "system";
|
||||
export type AppLanguage = "en" | "de";
|
||||
export type PackagePriority = "high" | "normal" | "low";
|
||||
export type ExtractCpuPriority = "high" | "middle" | "low";
|
||||
@@ -87,6 +88,10 @@ export interface StatisticsRolling24Hours {
|
||||
export interface StatisticsLedger {
|
||||
version: 2;
|
||||
startedAt: number;
|
||||
minuteTrackingStartedAt?: number;
|
||||
providerSeedSuppressedDay?: string;
|
||||
providerSeedBaselineBytes?: Partial<Record<DebridProvider, number>>;
|
||||
providerBytesOnlyDays?: string[];
|
||||
days: StatisticsDayBucket[];
|
||||
minutes: StatisticsMinuteBucket[];
|
||||
}
|
||||
@@ -202,6 +207,7 @@ export interface AppSettings extends DailyStartSettings {
|
||||
clipboardWatch: boolean;
|
||||
minimizeToTray: boolean;
|
||||
theme: AppTheme;
|
||||
themePreference: ThemePreference;
|
||||
logStorageLocation: LogStorageLocation;
|
||||
collapseNewPackages: boolean;
|
||||
animatePackageDisclosure: boolean;
|
||||
@@ -336,6 +342,7 @@ export interface RendererSettings extends DailyStartSettings {
|
||||
clipboardWatch: boolean;
|
||||
minimizeToTray: boolean;
|
||||
theme: AppTheme;
|
||||
themePreference: ThemePreference;
|
||||
logStorageLocation: LogStorageLocation;
|
||||
collapseNewPackages: boolean;
|
||||
animatePackageDisclosure: boolean;
|
||||
@@ -687,6 +694,8 @@ export interface UiSnapshot {
|
||||
clipboardActive: boolean;
|
||||
reconnectSeconds: number;
|
||||
packageSpeedBps: Record<string, number>;
|
||||
runRemainingBytes?: number;
|
||||
runRemainingUnknownItems?: number;
|
||||
diskWaitEvents?: Array<{
|
||||
phase: "download" | "extract" | "remux";
|
||||
ownerId: string;
|
||||
@@ -698,8 +707,9 @@ export interface UiSnapshot {
|
||||
deficitBytes: number;
|
||||
retryAt: number;
|
||||
}>;
|
||||
snapshotRevision?: number;
|
||||
payloadKind?: "full" | "delta";
|
||||
removedItemIds?: string[];
|
||||
removedItemIds?: string[];
|
||||
removedPackageIds?: string[];
|
||||
rotationEvents?: RotationEvent[];
|
||||
accountRuntime?: AccountRuntimeEntry[];
|
||||
|
||||
Reference in New Issue
Block a user