fix: harden diagnostics persistence and update downloads
This commit is contained in:
+321
-112
@@ -75,16 +75,30 @@ function isPathInsideDir(filePath: string, dirPath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSessionTargetPath(value: unknown, packageOutputDir: string): string {
|
||||
const targetPath = asText(value);
|
||||
if (!targetPath || !packageOutputDir || !path.isAbsolute(targetPath)) {
|
||||
function normalizeSessionTargetPath(value: unknown, packageOutputDir: string): string {
|
||||
const targetPath = asText(value);
|
||||
if (!targetPath || !packageOutputDir || !path.isAbsolute(targetPath)) {
|
||||
return "";
|
||||
}
|
||||
if (!isPathInsideDir(targetPath, packageOutputDir)) {
|
||||
return "";
|
||||
}
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
|
||||
function normalizeSessionMetadataRenameTargetPath(value: unknown, packageOutputDir: string): string {
|
||||
if (!path.isAbsolute(packageOutputDir)) {
|
||||
return "";
|
||||
}
|
||||
const targetPath = normalizeSessionTargetPath(value, packageOutputDir);
|
||||
if (!targetPath) {
|
||||
return "";
|
||||
}
|
||||
const resolvedOutputDir = path.resolve(packageOutputDir);
|
||||
const normalizedTargetPath = process.platform === "win32" ? targetPath.toLowerCase() : targetPath;
|
||||
const normalizedOutputDir = process.platform === "win32" ? resolvedOutputDir.toLowerCase() : resolvedOutputDir;
|
||||
return normalizedTargetPath === normalizedOutputDir ? "" : targetPath;
|
||||
}
|
||||
|
||||
function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
|
||||
const num = Number(value);
|
||||
@@ -907,9 +921,16 @@ function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
||||
return parsed;
|
||||
}
|
||||
const preferApi = parsed.megaDebridPreferApi !== undefined ? Boolean(parsed.megaDebridPreferApi) : true;
|
||||
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
|
||||
}
|
||||
|
||||
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
|
||||
}
|
||||
|
||||
const PERSISTED_SETTINGS_KEYS = new Set(Object.keys(defaultSettings()));
|
||||
|
||||
function isPersistedSettingsEnvelope(value: unknown): boolean {
|
||||
const parsed = asRecord(value);
|
||||
return Boolean(parsed && Object.keys(parsed).some((key) => PERSISTED_SETTINGS_KEYS.has(key)));
|
||||
}
|
||||
|
||||
interface LoadedSettingsFile {
|
||||
settings: AppSettings;
|
||||
needsCredentialRewrite: boolean;
|
||||
@@ -917,7 +938,12 @@ interface LoadedSettingsFile {
|
||||
|
||||
function readSettingsFile(filePath: string): LoadedSettingsFile | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
||||
const raw = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
if (!isPersistedSettingsEnvelope(raw)) {
|
||||
logger.error(`Settings-Datei beschädigt (Struktur ungültig): ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
const parsed = raw as AppSettings;
|
||||
const needsCredentialRewrite = needsPersistedSettingsRewrite(parsed);
|
||||
const restored = restorePersistedSettings(parsed);
|
||||
const migratedLanguage = (restored as Partial<AppSettings>).language === undefined ? "de" : restored.language;
|
||||
@@ -1001,10 +1027,11 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
|
||||
speedBps: clampNumber(item.speedBps, 0, 0, 10_000_000_000),
|
||||
downloadedBytes: clampNumber(item.downloadedBytes, 0, 0, 10_000_000_000_000),
|
||||
totalBytes: item.totalBytes == null ? null : clampNumber(item.totalBytes, 0, 0, 10_000_000_000_000),
|
||||
progressPercent: clampNumber(item.progressPercent, 0, 0, 100),
|
||||
fileName: asText(item.fileName) || "download.bin",
|
||||
targetPath: asText(item.targetPath),
|
||||
resumable: item.resumable === undefined ? true : Boolean(item.resumable),
|
||||
progressPercent: clampNumber(item.progressPercent, 0, 0, 100),
|
||||
fileName: asText(item.fileName) || "download.bin",
|
||||
targetPath: asText(item.targetPath),
|
||||
metadataRenameTargetPath: asText(item.metadataRenameTargetPath) || undefined,
|
||||
resumable: item.resumable === undefined ? true : Boolean(item.resumable),
|
||||
attempts: clampNumber(item.attempts, 0, 0, 10_000),
|
||||
lastError: asText(item.lastError),
|
||||
fullStatus: asText(item.fullStatus),
|
||||
@@ -1083,21 +1110,35 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
|
||||
logger.warn(`normalizeLoadedSession: ${orphanedItemCount} verwaiste Items entfernt (fehlende Pakete)`);
|
||||
}
|
||||
|
||||
let droppedUnsafeTargetPathCount = 0;
|
||||
for (const item of Object.values(itemsById)) {
|
||||
const pkg = packagesById[item.packageId];
|
||||
if (!pkg) {
|
||||
let droppedUnsafeTargetPathCount = 0;
|
||||
let droppedUnsafeMetadataRenameTargetPathCount = 0;
|
||||
for (const item of Object.values(itemsById)) {
|
||||
const pkg = packagesById[item.packageId];
|
||||
if (!pkg) {
|
||||
continue;
|
||||
}
|
||||
const safeTargetPath = normalizeSessionTargetPath(item.targetPath, pkg.outputDir);
|
||||
if (!safeTargetPath && asText(item.targetPath)) {
|
||||
droppedUnsafeTargetPathCount += 1;
|
||||
}
|
||||
item.targetPath = safeTargetPath;
|
||||
}
|
||||
if (droppedUnsafeTargetPathCount > 0) {
|
||||
logger.warn(`normalizeLoadedSession: ${droppedUnsafeTargetPathCount} unsichere targetPath-Eintraege verworfen`);
|
||||
}
|
||||
droppedUnsafeTargetPathCount += 1;
|
||||
}
|
||||
item.targetPath = safeTargetPath;
|
||||
const metadataRenameTargetPath = asText(item.metadataRenameTargetPath);
|
||||
const safeMetadataRenameTargetPath = normalizeSessionMetadataRenameTargetPath(metadataRenameTargetPath, pkg.outputDir);
|
||||
if (metadataRenameTargetPath && !safeMetadataRenameTargetPath) {
|
||||
droppedUnsafeMetadataRenameTargetPathCount += 1;
|
||||
}
|
||||
if (safeMetadataRenameTargetPath) {
|
||||
item.metadataRenameTargetPath = safeMetadataRenameTargetPath;
|
||||
} else {
|
||||
delete item.metadataRenameTargetPath;
|
||||
}
|
||||
}
|
||||
if (droppedUnsafeTargetPathCount > 0) {
|
||||
logger.warn(`normalizeLoadedSession: ${droppedUnsafeTargetPathCount} unsichere targetPath-Eintraege verworfen`);
|
||||
}
|
||||
if (droppedUnsafeMetadataRenameTargetPathCount > 0) {
|
||||
logger.warn(`normalizeLoadedSession: ${droppedUnsafeMetadataRenameTargetPathCount} unsichere metadataRenameTargetPath-Einträge verworfen`);
|
||||
}
|
||||
|
||||
for (const pkg of Object.values(packagesById)) {
|
||||
pkg.itemIds = pkg.itemIds.filter((itemId) => {
|
||||
@@ -1142,14 +1183,13 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
|
||||
}
|
||||
|
||||
export function loadSettings(paths: StoragePaths): AppSettings {
|
||||
ensureBaseDir(paths.baseDir);
|
||||
if (!fs.existsSync(paths.configFile)) {
|
||||
return defaultSettings();
|
||||
}
|
||||
const loaded = readSettingsFile(paths.configFile);
|
||||
ensureBaseDir(paths.baseDir);
|
||||
const primaryExists = fs.existsSync(paths.configFile);
|
||||
const backupFile = `${paths.configFile}.bak`;
|
||||
const backupExists = fs.existsSync(backupFile);
|
||||
const loaded = primaryExists ? readSettingsFile(paths.configFile) : null;
|
||||
if (loaded) {
|
||||
const backupFile = `${paths.configFile}.bak`;
|
||||
const backupNeedsCredentialRewrite = fs.existsSync(backupFile)
|
||||
const backupNeedsCredentialRewrite = backupExists
|
||||
? needsSettingsFileCredentialRewrite(backupFile)
|
||||
: false;
|
||||
if (loaded.needsCredentialRewrite || backupNeedsCredentialRewrite) {
|
||||
@@ -1158,17 +1198,20 @@ export function loadSettings(paths: StoragePaths): AppSettings {
|
||||
return loaded.settings;
|
||||
}
|
||||
|
||||
const backupFile = `${paths.configFile}.bak`;
|
||||
const backupLoaded = fs.existsSync(backupFile) ? readSettingsFile(backupFile) : null;
|
||||
const backupLoaded = backupExists ? readSettingsFile(backupFile) : null;
|
||||
if (backupLoaded) {
|
||||
logger.warn("Konfiguration defekt, Backup-Datei wird verwendet");
|
||||
logger.warn(primaryExists
|
||||
? "Konfiguration defekt, Backup-Datei wird verwendet"
|
||||
: "Konfiguration fehlt, Backup-Datei wird verwendet");
|
||||
rewriteProtectedSettings(paths, backupLoaded.settings);
|
||||
return backupLoaded.settings;
|
||||
}
|
||||
|
||||
logger.error("Konfiguration konnte nicht geladen werden (auch Backup fehlgeschlagen)");
|
||||
return defaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryExists || backupExists) {
|
||||
logger.error("Konfiguration konnte nicht geladen werden (auch Backup fehlgeschlagen)");
|
||||
}
|
||||
return defaultSettings();
|
||||
}
|
||||
|
||||
function syncRenameWithExdevFallback(tempPath: string, targetPath: string): void {
|
||||
try {
|
||||
@@ -1282,6 +1325,16 @@ interface LoadedSessionFile {
|
||||
wasRunning: boolean;
|
||||
}
|
||||
|
||||
function isPersistedSessionEnvelope(value: unknown): boolean {
|
||||
const parsed = asRecord(value);
|
||||
return Boolean(
|
||||
parsed
|
||||
&& Array.isArray(parsed.packageOrder)
|
||||
&& asRecord(parsed.packages)
|
||||
&& asRecord(parsed.items)
|
||||
);
|
||||
}
|
||||
|
||||
function readSessionFile(filePath: string): LoadedSessionFile | null {
|
||||
let raw: string | null = null;
|
||||
const maxAttempts = 5;
|
||||
@@ -1307,9 +1360,13 @@ function readSessionFile(filePath: string): LoadedSessionFile | null {
|
||||
}
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!isPersistedSessionEnvelope(parsed)) {
|
||||
logger.error(`Session-Datei beschädigt (Struktur ungültig): ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeLoadedSession(parsed);
|
||||
const wasRunning = normalized.running;
|
||||
const session = normalizeLoadedSessionTransientFields(normalized);
|
||||
@@ -1331,8 +1388,19 @@ export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
|
||||
writeSettingsFileAtomically(paths.configFile, payloads.primary);
|
||||
}
|
||||
|
||||
let asyncSettingsSaveRunning = false;
|
||||
let asyncSettingsSaveQueued: { paths: StoragePaths; settings: AppSettings; generation: number } | null = null;
|
||||
interface AsyncSaveWaiter {
|
||||
resolve: () => void;
|
||||
reject: (error: unknown) => void;
|
||||
}
|
||||
|
||||
interface QueuedSettingsSave {
|
||||
paths: StoragePaths;
|
||||
settings: AppSettings;
|
||||
generation: number;
|
||||
waiters: AsyncSaveWaiter[];
|
||||
}
|
||||
|
||||
let asyncSettingsSaveQueued: QueuedSettingsSave | null = null;
|
||||
let syncSettingsSaveGeneration = 0;
|
||||
let activeSettingsSave: Promise<void> | null = null;
|
||||
|
||||
@@ -1364,27 +1432,52 @@ async function writeSettingsPayload(paths: StoragePaths, settings: AppSettings,
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettingsPayloadAsync(paths: StoragePaths, settings: AppSettings, generation: number): Promise<void> {
|
||||
if (asyncSettingsSaveRunning) {
|
||||
asyncSettingsSaveQueued = { paths, settings, generation };
|
||||
function startSettingsSaveDrain(): void {
|
||||
if (activeSettingsSave) {
|
||||
return;
|
||||
}
|
||||
asyncSettingsSaveRunning = true;
|
||||
const operation = writeSettingsPayload(paths, settings, generation).catch((error) => {
|
||||
logger.error(`Async Settings-Save fehlgeschlagen: ${String(error)}`);
|
||||
}).finally(() => {
|
||||
asyncSettingsSaveRunning = false;
|
||||
let operation: Promise<void>;
|
||||
operation = (async () => {
|
||||
while (asyncSettingsSaveQueued) {
|
||||
const current = asyncSettingsSaveQueued;
|
||||
asyncSettingsSaveQueued = null;
|
||||
try {
|
||||
await writeSettingsPayload(current.paths, current.settings, current.generation);
|
||||
current.waiters.forEach((waiter) => waiter.resolve());
|
||||
} catch (error) {
|
||||
current.waiters.forEach((waiter) => waiter.reject(error));
|
||||
const pending = asyncSettingsSaveQueued as QueuedSettingsSave | null;
|
||||
asyncSettingsSaveQueued = null;
|
||||
pending?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
logger.error(`Async Settings-Save fehlgeschlagen: ${String(error)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
if (activeSettingsSave === operation) {
|
||||
activeSettingsSave = null;
|
||||
}
|
||||
if (asyncSettingsSaveQueued) {
|
||||
const queued = asyncSettingsSaveQueued;
|
||||
asyncSettingsSaveQueued = null;
|
||||
void saveSettingsPayloadAsync(queued.paths, queued.settings, queued.generation);
|
||||
startSettingsSaveDrain();
|
||||
}
|
||||
});
|
||||
activeSettingsSave = operation;
|
||||
await operation;
|
||||
void operation.catch(() => {});
|
||||
}
|
||||
|
||||
function saveSettingsPayloadAsync(paths: StoragePaths, settings: AppSettings, generation: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const waiter = { resolve, reject };
|
||||
if (asyncSettingsSaveQueued) {
|
||||
asyncSettingsSaveQueued.paths = paths;
|
||||
asyncSettingsSaveQueued.settings = settings;
|
||||
asyncSettingsSaveQueued.generation = generation;
|
||||
asyncSettingsSaveQueued.waiters.push(waiter);
|
||||
} else {
|
||||
asyncSettingsSaveQueued = { paths, settings, generation, waiters: [waiter] };
|
||||
}
|
||||
startSettingsSaveDrain();
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveSettingsAsync(paths: StoragePaths, settings: AppSettings): Promise<void> {
|
||||
@@ -1446,25 +1539,8 @@ export function loadSessionWithStatus(paths: StoragePaths): SessionLoadResult {
|
||||
const primary = primaryExists ? readSessionFile(paths.sessionFile) : null;
|
||||
|
||||
if (primary) {
|
||||
const primaryPkgCount = Object.keys(primary.session.packages).length;
|
||||
if (primaryPkgCount === 0 && backupExists) {
|
||||
const backup = readSessionFile(backupFile);
|
||||
if (backup) {
|
||||
const backupPkgCount = Object.keys(backup.session.packages).length;
|
||||
if (backupPkgCount > 0) {
|
||||
logger.warn(`Session-Datei ist leer (0 Pakete), aber Backup hat ${backupPkgCount} Pakete — verwende Backup`);
|
||||
try {
|
||||
const payload = JSON.stringify({ ...backup.session, updatedAt: Date.now() }, safeJsonReplacer);
|
||||
fs.writeFileSync(syncTempFile, payload, "utf8");
|
||||
syncRenameWithExdevFallback(syncTempFile, paths.sessionFile);
|
||||
} catch {
|
||||
}
|
||||
return { session: backup.session, status: "recovered-backup", wasRunning: backup.wasRunning };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { session: primary.session, status: "ok", wasRunning: primary.wasRunning };
|
||||
}
|
||||
}
|
||||
|
||||
const backup = backupExists ? readSessionFile(backupFile) : null;
|
||||
if (backup) {
|
||||
@@ -1532,8 +1608,14 @@ export function saveSession(paths: StoragePaths, session: SessionState): void {
|
||||
}
|
||||
}
|
||||
|
||||
let asyncSaveRunning = false;
|
||||
let asyncSaveQueued: { paths: StoragePaths; payload: string; generation: number } | null = null;
|
||||
interface QueuedSessionSave {
|
||||
paths: StoragePaths;
|
||||
payload: string;
|
||||
generation: number;
|
||||
waiters: AsyncSaveWaiter[];
|
||||
}
|
||||
|
||||
let asyncSaveQueued: QueuedSessionSave | null = null;
|
||||
let syncSaveGeneration = 0;
|
||||
let activeSessionSave: Promise<void> | null = null;
|
||||
|
||||
@@ -1568,27 +1650,52 @@ async function writeSessionPayload(paths: StoragePaths, payload: string, generat
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSessionPayloadAsync(paths: StoragePaths, payload: string, generation: number): Promise<void> {
|
||||
if (asyncSaveRunning) {
|
||||
asyncSaveQueued = { paths, payload, generation };
|
||||
function startSessionSaveDrain(): void {
|
||||
if (activeSessionSave) {
|
||||
return;
|
||||
}
|
||||
asyncSaveRunning = true;
|
||||
const operation = writeSessionPayload(paths, payload, generation).catch((error) => {
|
||||
logger.error(`Async Session-Save fehlgeschlagen: ${String(error)}`);
|
||||
}).finally(() => {
|
||||
asyncSaveRunning = false;
|
||||
let operation: Promise<void>;
|
||||
operation = (async () => {
|
||||
while (asyncSaveQueued) {
|
||||
const current = asyncSaveQueued;
|
||||
asyncSaveQueued = null;
|
||||
try {
|
||||
await writeSessionPayload(current.paths, current.payload, current.generation);
|
||||
current.waiters.forEach((waiter) => waiter.resolve());
|
||||
} catch (error) {
|
||||
current.waiters.forEach((waiter) => waiter.reject(error));
|
||||
const pending = asyncSaveQueued as QueuedSessionSave | null;
|
||||
asyncSaveQueued = null;
|
||||
pending?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
logger.error(`Async Session-Save fehlgeschlagen: ${String(error)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
if (activeSessionSave === operation) {
|
||||
activeSessionSave = null;
|
||||
}
|
||||
if (asyncSaveQueued) {
|
||||
const queued = asyncSaveQueued;
|
||||
asyncSaveQueued = null;
|
||||
void saveSessionPayloadAsync(queued.paths, queued.payload, queued.generation);
|
||||
startSessionSaveDrain();
|
||||
}
|
||||
});
|
||||
activeSessionSave = operation;
|
||||
await operation;
|
||||
void operation.catch(() => {});
|
||||
}
|
||||
|
||||
function saveSessionPayloadAsync(paths: StoragePaths, payload: string, generation: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const waiter = { resolve, reject };
|
||||
if (asyncSaveQueued) {
|
||||
asyncSaveQueued.paths = paths;
|
||||
asyncSaveQueued.payload = payload;
|
||||
asyncSaveQueued.generation = generation;
|
||||
asyncSaveQueued.waiters.push(waiter);
|
||||
} else {
|
||||
asyncSaveQueued = { paths, payload, generation, waiters: [waiter] };
|
||||
}
|
||||
startSessionSaveDrain();
|
||||
});
|
||||
}
|
||||
|
||||
interface BlockedSettingsSave {
|
||||
@@ -1616,6 +1723,47 @@ export interface PersistenceBarrier {
|
||||
|
||||
let activePersistenceBarrier: PersistenceBarrierState | null = null;
|
||||
|
||||
interface DetachedAsyncSaves {
|
||||
settings: QueuedSettingsSave | null;
|
||||
session: QueuedSessionSave | null;
|
||||
}
|
||||
|
||||
function detachPendingAsyncSaves(): DetachedAsyncSaves {
|
||||
const detached = {
|
||||
settings: asyncSettingsSaveQueued,
|
||||
session: asyncSaveQueued
|
||||
};
|
||||
asyncSettingsSaveQueued = null;
|
||||
asyncSaveQueued = null;
|
||||
return detached;
|
||||
}
|
||||
|
||||
function rejectDetachedAsyncSaves(detached: DetachedAsyncSaves, error: unknown): void {
|
||||
detached.settings?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
detached.session?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
}
|
||||
|
||||
function requeueDetachedAsyncSaves(detached: DetachedAsyncSaves): void {
|
||||
if (detached.settings) {
|
||||
detached.settings.generation = syncSettingsSaveGeneration;
|
||||
if (asyncSettingsSaveQueued) {
|
||||
asyncSettingsSaveQueued.waiters.unshift(...detached.settings.waiters);
|
||||
} else {
|
||||
asyncSettingsSaveQueued = detached.settings;
|
||||
}
|
||||
startSettingsSaveDrain();
|
||||
}
|
||||
if (detached.session) {
|
||||
detached.session.generation = syncSaveGeneration;
|
||||
if (asyncSaveQueued) {
|
||||
asyncSaveQueued.waiters.unshift(...detached.session.waiters);
|
||||
} else {
|
||||
asyncSaveQueued = detached.session;
|
||||
}
|
||||
startSessionSaveDrain();
|
||||
}
|
||||
}
|
||||
|
||||
function blockSettingsSave(state: PersistenceBarrierState, paths: StoragePaths, settings: AppSettings): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const waiter = { resolve, reject };
|
||||
@@ -1643,28 +1791,61 @@ function blockSessionSave(state: PersistenceBarrierState, paths: StoragePaths, p
|
||||
}
|
||||
|
||||
async function replayBlockedSaves(state: PersistenceBarrierState): Promise<void> {
|
||||
let firstError: unknown;
|
||||
let failed = false;
|
||||
while (state.blockedSettings || state.blockedSession) {
|
||||
const blockedSettings = state.blockedSettings;
|
||||
const blockedSession = state.blockedSession;
|
||||
state.blockedSettings = null;
|
||||
state.blockedSession = null;
|
||||
try {
|
||||
await Promise.all([
|
||||
blockedSettings
|
||||
? saveSettingsPayloadAsync(blockedSettings.paths, blockedSettings.settings, syncSettingsSaveGeneration)
|
||||
: Promise.resolve(),
|
||||
blockedSession
|
||||
? saveSessionPayloadAsync(blockedSession.paths, blockedSession.payload, syncSaveGeneration)
|
||||
: Promise.resolve()
|
||||
]);
|
||||
blockedSettings?.waiters.forEach((waiter) => waiter.resolve());
|
||||
blockedSession?.waiters.forEach((waiter) => waiter.resolve());
|
||||
} catch (error) {
|
||||
blockedSettings?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
blockedSession?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
throw error;
|
||||
const [settingsResult, sessionResult] = await Promise.allSettled([
|
||||
blockedSettings
|
||||
? saveSettingsPayloadAsync(blockedSettings.paths, blockedSettings.settings, syncSettingsSaveGeneration)
|
||||
: Promise.resolve(),
|
||||
blockedSession
|
||||
? saveSessionPayloadAsync(blockedSession.paths, blockedSession.payload, syncSaveGeneration)
|
||||
: Promise.resolve()
|
||||
]);
|
||||
if (blockedSettings) {
|
||||
if (settingsResult.status === "fulfilled") {
|
||||
blockedSettings.waiters.forEach((waiter) => waiter.resolve());
|
||||
} else {
|
||||
blockedSettings.waiters.forEach((waiter) => waiter.reject(settingsResult.reason));
|
||||
if (!failed) {
|
||||
firstError = settingsResult.reason;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blockedSession) {
|
||||
if (sessionResult.status === "fulfilled") {
|
||||
blockedSession.waiters.forEach((waiter) => waiter.resolve());
|
||||
} else {
|
||||
blockedSession.waiters.forEach((waiter) => waiter.reject(sessionResult.reason));
|
||||
if (!failed) {
|
||||
firstError = sessionResult.reason;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
throw firstError;
|
||||
}
|
||||
}
|
||||
|
||||
function rejectBlockedSaves(state: PersistenceBarrierState, error: unknown): void {
|
||||
state.blockedSettings?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
state.blockedSession?.waiters.forEach((waiter) => waiter.reject(error));
|
||||
state.blockedSettings = null;
|
||||
state.blockedSession = null;
|
||||
}
|
||||
|
||||
function resolveBlockedSaves(state: PersistenceBarrierState): void {
|
||||
state.blockedSettings?.waiters.forEach((waiter) => waiter.resolve());
|
||||
state.blockedSession?.waiters.forEach((waiter) => waiter.resolve());
|
||||
state.blockedSettings = null;
|
||||
state.blockedSession = null;
|
||||
}
|
||||
|
||||
export async function acquirePersistenceBarrier(): Promise<PersistenceBarrier> {
|
||||
@@ -1680,19 +1861,46 @@ export async function acquirePersistenceBarrier(): Promise<PersistenceBarrier> {
|
||||
resolveReleased
|
||||
};
|
||||
activePersistenceBarrier = state;
|
||||
cancelPendingAsyncSaves();
|
||||
await Promise.all([activeSettingsSave ?? Promise.resolve(), activeSessionSave ?? Promise.resolve()]);
|
||||
const detachedSaves = detachPendingAsyncSaves();
|
||||
const activeWriteResults = await Promise.allSettled([
|
||||
activeSettingsSave ?? Promise.resolve(),
|
||||
activeSessionSave ?? Promise.resolve()
|
||||
]);
|
||||
const activeWriteFailure = activeWriteResults.find((result) => result.status === "rejected");
|
||||
if (activeWriteFailure?.status === "rejected") {
|
||||
rejectBlockedSaves(state, activeWriteFailure.reason);
|
||||
if (activePersistenceBarrier === state) {
|
||||
activePersistenceBarrier = null;
|
||||
}
|
||||
requeueDetachedAsyncSaves(detachedSaves);
|
||||
state.resolveReleased();
|
||||
throw activeWriteFailure.reason;
|
||||
}
|
||||
rejectDetachedAsyncSaves(detachedSaves, new Error("Persistenzbarriere hat ausstehenden Save verworfen"));
|
||||
let finished = false;
|
||||
return {
|
||||
release: async ({ replayBlocked }) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
let replayError: unknown;
|
||||
let replayFailed = false;
|
||||
try {
|
||||
if (replayBlocked) {
|
||||
await replayBlockedSaves(state);
|
||||
do {
|
||||
try {
|
||||
await replayBlockedSaves(state);
|
||||
} catch (error) {
|
||||
if (!replayFailed) {
|
||||
replayError = error;
|
||||
replayFailed = true;
|
||||
}
|
||||
}
|
||||
} while (state.blockedSettings || state.blockedSession);
|
||||
} else {
|
||||
state.blockedSettings?.waiters.forEach((waiter) => waiter.resolve());
|
||||
state.blockedSession?.waiters.forEach((waiter) => waiter.resolve());
|
||||
resolveBlockedSaves(state);
|
||||
}
|
||||
if (replayFailed) {
|
||||
throw replayError;
|
||||
}
|
||||
} finally {
|
||||
if (activePersistenceBarrier === state) {
|
||||
@@ -1704,12 +1912,13 @@ export async function acquirePersistenceBarrier(): Promise<PersistenceBarrier> {
|
||||
};
|
||||
}
|
||||
|
||||
export function cancelPendingAsyncSaves(): void {
|
||||
asyncSaveQueued = null;
|
||||
asyncSettingsSaveQueued = null;
|
||||
syncSaveGeneration += 1;
|
||||
syncSettingsSaveGeneration += 1;
|
||||
}
|
||||
export function cancelPendingAsyncSaves(): void {
|
||||
syncSettingsSaveGeneration += 1;
|
||||
syncSaveGeneration += 1;
|
||||
const detached = detachPendingAsyncSaves();
|
||||
detached.session?.waiters.forEach((waiter) => waiter.resolve());
|
||||
detached.settings?.waiters.forEach((waiter) => waiter.resolve());
|
||||
}
|
||||
|
||||
export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> {
|
||||
const payload = JSON.stringify({ ...session, updatedAt: Date.now() }, safeJsonReplacer);
|
||||
|
||||
Reference in New Issue
Block a user