fix: project got corrupted sometimes

This commit is contained in:
2026-02-22 10:27:28 +01:00
parent 03657e7a84
commit f43e150668
8 changed files with 176 additions and 17 deletions

View File

@@ -184,9 +184,19 @@ export class MediaEngine extends EventEmitter {
}
setProjectContext(projectId: string, dataDir?: string, internalDir?: string): void {
const nextDataDir = dataDir || null;
const nextInternalDir = internalDir || null;
if (
this.currentProjectId === projectId
&& this.dataDir === nextDataDir
&& this.internalDir === nextInternalDir
) {
return;
}
this.currentProjectId = projectId;
this.dataDir = dataDir || null;
this.internalDir = internalDir || null;
this.dataDir = nextDataDir;
this.internalDir = nextInternalDir;
console.log(`[MediaEngine] setProjectContext: projectId=${projectId}, dataDir=${this.dataDir}, internalDir=${this.internalDir}`);
}

View File

@@ -176,6 +176,7 @@ export class MetaEngine extends EventEmitter {
private categories: Set<string> = new Set();
private projectMetadata: ProjectMetadata | null = null;
private initialized: boolean = false;
private startupSyncPromise: Promise<void> | null = null;
constructor() {
super();
@@ -218,13 +219,19 @@ export class MetaEngine extends EventEmitter {
}
setProjectContext(projectId: string, dataDir?: string): void {
const nextDataDir = dataDir || null;
if (this.currentProjectId === projectId && this.dataDir === nextDataDir) {
return;
}
this.currentProjectId = projectId;
this.dataDir = dataDir || null;
this.dataDir = nextDataDir;
// Reset in-memory cache when project changes
this.tags.clear();
this.categories.clear();
this.projectMetadata = null;
this.initialized = false;
this.startupSyncPromise = null;
}
getProjectContext(): string {
@@ -394,8 +401,7 @@ export class MetaEngine extends EventEmitter {
try {
await this.ensureMetaDirExists();
const filePath = this.getCategoriesFilePath();
const content = JSON.stringify(Array.from(this.categories).sort(), null, 2);
await fs.writeFile(filePath, content, 'utf-8');
await this.writeJsonFileAtomically(filePath, Array.from(this.categories).sort());
} catch (error) {
console.error('[MetaEngine] Failed to save categories:', error);
throw error;
@@ -415,8 +421,7 @@ export class MetaEngine extends EventEmitter {
categorySettings: _categorySettings,
...persistedMetadata
} = this.projectMetadata || {};
const content = JSON.stringify(persistedMetadata, null, 2);
await fs.writeFile(filePath, content, 'utf-8');
await this.writeJsonFileAtomically(filePath, persistedMetadata);
} catch (error) {
console.error('[MetaEngine] Failed to save project metadata:', error);
throw error;
@@ -433,8 +438,7 @@ export class MetaEngine extends EventEmitter {
const metadata = this.ensureCategoryMetadataForKnownCategories(
this.projectMetadata?.categoryMetadata,
);
const content = JSON.stringify(metadata, null, 2);
await fs.writeFile(filePath, content, 'utf-8');
await this.writeJsonFileAtomically(filePath, metadata);
} catch (error) {
console.error('[MetaEngine] Failed to save category metadata:', error);
throw error;
@@ -582,6 +586,24 @@ export class MetaEngine extends EventEmitter {
}
}
private async writeJsonFileAtomically(filePath: string, value: unknown): Promise<void> {
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
const content = JSON.stringify(value, null, 2);
await fs.writeFile(tempPath, content, 'utf-8');
try {
await fs.rename(tempPath, filePath);
} catch (error) {
try {
await fs.unlink(tempPath);
} catch {
// Ignore cleanup errors.
}
throw error;
}
}
private ensureCategoryMetadataForKnownCategories(
categoryMetadata: Record<string, CategoryMetadata> | undefined,
): Record<string, CategoryMetadata> {
@@ -611,6 +633,24 @@ export class MetaEngine extends EventEmitter {
* - Project metadata: read from file or create from database
*/
async syncOnStartup(): Promise<void> {
if (this.initialized) {
return;
}
if (this.startupSyncPromise) {
await this.startupSyncPromise;
return;
}
this.startupSyncPromise = this.performSyncOnStartup();
try {
await this.startupSyncPromise;
} finally {
this.startupSyncPromise = null;
}
}
private async performSyncOnStartup(): Promise<void> {
console.log(`[MetaEngine] Syncing metadata for project: ${this.currentProjectId}`);
await this.ensureMetaDirExists();

View File

@@ -156,6 +156,10 @@ export class PostMediaEngine extends EventEmitter {
* Set the current project context
*/
setProjectContext(projectId: string): void {
if (this.currentProjectId === projectId) {
return;
}
this.currentProjectId = projectId;
console.log(`[PostMediaEngine] setProjectContext: projectId=${projectId}`);
}

View File

@@ -210,6 +210,15 @@ export class PreviewServer {
}
try {
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
const pathname = decodeURIComponent(requestUrl.pathname.replace(/\/+$/, '') || '/');
const asset = await this.resolveAsset(pathname);
if (asset) {
this.respondAsset(res, asset.contentType, asset.body);
return;
}
const context = await this.getActiveProjectContext();
this.postEngine.setProjectContext(context.projectId, context.dataDir);
this.mediaEngine.setProjectContext?.(context.projectId, context.dataDir, context.dataDir);
@@ -230,7 +239,6 @@ export class PreviewServer {
const language = metadata?.mainLanguage?.trim() || 'en';
const pageTitle = resolvePageTitle(metadata, context.projectName, context.projectDescription);
const maxPostsPerPage = clampMaxPostsPerPage(metadata?.maxPostsPerPage);
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
const requestTheme = sanitizePicoTheme(requestUrl.searchParams.get('theme'));
const previewThemeMode = sanitizePicoThemeMode(requestUrl.searchParams.get('mode'));
const useDraftContent = requestUrl.searchParams.get('draft') === 'true';
@@ -238,7 +246,6 @@ export class PreviewServer {
const appliedTheme = requestTheme ?? sanitizePicoTheme((metadata as { picoTheme?: unknown } | null)?.picoTheme);
const picoStylesheetHref = getPicoStylesheetHref(appliedTheme);
const htmlRewriteContext = await this.buildHtmlRewriteContext();
const pathname = decodeURIComponent(requestUrl.pathname.replace(/\/+$/, '') || '/');
if (pathname === '/__style-preview') {
const stylePreviewHtml = await this.renderStylePreview(htmlRewriteContext, {
@@ -252,12 +259,6 @@ export class PreviewServer {
return;
}
const asset = await this.resolveAsset(pathname);
if (asset) {
this.respondAsset(res, asset.contentType, asset.body);
return;
}
const imageAsset = await this.resolveImageAsset(pathname);
if (imageAsset) {
this.respondAsset(res, imageAsset.contentType, imageAsset.body);