feat: more on incremental rendering
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { getDatabase } from './connection';
|
||||
|
||||
export interface GeneratedFileHashRecord {
|
||||
contentHash: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export async function getGeneratedFileHash(projectId: string, relativePath: string): Promise<string | null> {
|
||||
const client = getDatabase().getLocalClient();
|
||||
if (!client) {
|
||||
@@ -18,6 +23,33 @@ export async function getGeneratedFileHash(projectId: string, relativePath: stri
|
||||
return result.rows[0].content_hash;
|
||||
}
|
||||
|
||||
export async function getGeneratedFileHashRecord(projectId: string, relativePath: string): Promise<GeneratedFileHashRecord | null> {
|
||||
const client = getDatabase().getLocalClient();
|
||||
if (!client) {
|
||||
throw new Error('Database client not available');
|
||||
}
|
||||
|
||||
const result = await client.execute({
|
||||
sql: 'SELECT content_hash, updated_at FROM generated_file_hashes WHERE project_id = ? AND relative_path = ? LIMIT 1',
|
||||
args: [projectId, relativePath],
|
||||
});
|
||||
|
||||
const row = result.rows[0];
|
||||
if (!row || typeof row.content_hash !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawUpdatedAt = row.updated_at;
|
||||
const updatedAt = typeof rawUpdatedAt === 'number'
|
||||
? rawUpdatedAt
|
||||
: Number(rawUpdatedAt);
|
||||
|
||||
return {
|
||||
contentHash: row.content_hash,
|
||||
updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setGeneratedFileHash(projectId: string, relativePath: string, hash: string): Promise<void> {
|
||||
const client = getDatabase().getLocalClient();
|
||||
if (!client) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
buildRequestedArchiveMaps,
|
||||
selectRequestedPosts,
|
||||
} from './ApplyValidationDataService';
|
||||
import { getGeneratedFileHashRecord } from '../database/generatedFileHashStore';
|
||||
|
||||
const DEFAULT_MAX_POSTS_PER_PAGE = 50;
|
||||
const MIN_MAX_POSTS_PER_PAGE = 1;
|
||||
@@ -96,6 +97,7 @@ export interface SiteValidationReport {
|
||||
sitemapChanged: boolean;
|
||||
missingUrlPaths: string[];
|
||||
extraUrlPaths: string[];
|
||||
updatedPostUrlPaths: string[];
|
||||
expectedUrlCount: number;
|
||||
existingHtmlUrlCount: number;
|
||||
}
|
||||
@@ -375,6 +377,7 @@ export class BlogGenerationEngine {
|
||||
content,
|
||||
knownDirectories: knownOutputDirectories,
|
||||
hashCache: generatedHashCache,
|
||||
refreshHashTimestampOnUnchanged: true,
|
||||
});
|
||||
|
||||
let pagesGenerated = 0;
|
||||
@@ -551,19 +554,40 @@ export class BlogGenerationEngine {
|
||||
|
||||
onProgress(50, 'Comparing sitemap to html pages...');
|
||||
|
||||
const postTimestampChecks = await Promise.all(publishedPosts.map(async (post) => {
|
||||
const createdAt = resolvePostCreatedAt(post);
|
||||
const year = String(createdAt.getFullYear());
|
||||
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
|
||||
const postFilePath = path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`);
|
||||
const postUrlPath = buildCanonicalPostPath(post);
|
||||
const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`;
|
||||
const generatedRecord = await getGeneratedFileHashRecord(options.projectId, relativePath);
|
||||
|
||||
return {
|
||||
postUrlPath,
|
||||
postFilePath,
|
||||
generatedUpdatedAtMs: generatedRecord?.updatedAt,
|
||||
};
|
||||
}));
|
||||
|
||||
const diffResult = await compareSitemapToHtml({
|
||||
sitemapXml,
|
||||
baseUrl: options.baseUrl,
|
||||
htmlDir,
|
||||
postTimestampChecks,
|
||||
});
|
||||
|
||||
onProgress(100, `Validation complete (${diffResult.missingUrlPaths.length} missing, ${diffResult.extraUrlPaths.length} extra)`);
|
||||
onProgress(
|
||||
100,
|
||||
`Validation complete (${diffResult.missingUrlPaths.length} missing, ${diffResult.extraUrlPaths.length} extra, ${diffResult.updatedPostUrlPaths.length} updated)`
|
||||
);
|
||||
|
||||
return {
|
||||
sitemapPath,
|
||||
sitemapChanged,
|
||||
missingUrlPaths: diffResult.missingUrlPaths,
|
||||
extraUrlPaths: diffResult.extraUrlPaths,
|
||||
updatedPostUrlPaths: diffResult.updatedPostUrlPaths,
|
||||
expectedUrlCount: diffResult.expectedUrlCount,
|
||||
existingHtmlUrlCount: diffResult.existingHtmlUrlCount,
|
||||
};
|
||||
@@ -577,11 +601,13 @@ export class BlogGenerationEngine {
|
||||
onProgress(0, 'Applying validation changes...');
|
||||
|
||||
const missingPaths = Array.isArray(report.missingUrlPaths) ? report.missingUrlPaths : [];
|
||||
const updatedPostPaths = Array.isArray(report.updatedPostUrlPaths) ? report.updatedPostUrlPaths : [];
|
||||
const rerenderPaths = Array.from(new Set([...missingPaths, ...updatedPostPaths]));
|
||||
const extraPaths = Array.isArray(report.extraUrlPaths) ? report.extraUrlPaths : [];
|
||||
|
||||
onProgress(10, 'Planning validation apply steps...');
|
||||
|
||||
const missingPathPlan = planMissingValidationPaths(missingPaths);
|
||||
const missingPathPlan = planMissingValidationPaths(rerenderPaths);
|
||||
|
||||
onProgress(20, 'Deleting extra URLs...');
|
||||
|
||||
@@ -686,6 +712,7 @@ export class BlogGenerationEngine {
|
||||
htmlDir,
|
||||
urlPath,
|
||||
content,
|
||||
refreshHashTimestampOnUnchanged: true,
|
||||
});
|
||||
const onPageGenerated = (_message: string) => {
|
||||
// no-op for applyValidation
|
||||
|
||||
@@ -60,6 +60,7 @@ export async function writeFileIfHashChanged(params: {
|
||||
getGeneratedFileHash?: (projectId: string, relativePath: string) => Promise<string | null>;
|
||||
setGeneratedFileHash?: (projectId: string, relativePath: string, hash: string) => Promise<void>;
|
||||
computeHash?: (content: string) => string;
|
||||
refreshHashTimestampOnUnchanged?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const getHash = params.getGeneratedFileHash ?? getGeneratedFileHash;
|
||||
const setHash = params.setGeneratedFileHash ?? setGeneratedFileHash;
|
||||
@@ -75,6 +76,10 @@ export async function writeFileIfHashChanged(params: {
|
||||
}
|
||||
|
||||
if (previousHash === hash) {
|
||||
if (params.refreshHashTimestampOnUnchanged) {
|
||||
await setHash(params.projectId, params.relativePath, hash);
|
||||
params.hashCache?.set(params.relativePath, hash);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,6 +100,7 @@ export async function writeHtmlPage(params: {
|
||||
getGeneratedFileHash?: (projectId: string, relativePath: string) => Promise<string | null>;
|
||||
setGeneratedFileHash?: (projectId: string, relativePath: string, hash: string) => Promise<void>;
|
||||
computeHash?: (content: string) => string;
|
||||
refreshHashTimestampOnUnchanged?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const normalizedPath = params.urlPath.replace(/^\//, '');
|
||||
const filePath = normalizedPath
|
||||
@@ -124,6 +130,7 @@ export async function writeHtmlPage(params: {
|
||||
getGeneratedFileHash: params.getGeneratedFileHash,
|
||||
setGeneratedFileHash: params.setGeneratedFileHash,
|
||||
computeHash: params.computeHash,
|
||||
refreshHashTimestampOnUnchanged: params.refreshHashTimestampOnUnchanged,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,22 @@ import * as path from 'node:path';
|
||||
export interface SiteValidationDiffResult {
|
||||
missingUrlPaths: string[];
|
||||
extraUrlPaths: string[];
|
||||
updatedPostUrlPaths: string[];
|
||||
expectedUrlCount: number;
|
||||
existingHtmlUrlCount: number;
|
||||
}
|
||||
|
||||
export interface PostTimestampCheck {
|
||||
postUrlPath: string;
|
||||
postFilePath: string;
|
||||
generatedUpdatedAtMs?: number;
|
||||
}
|
||||
|
||||
interface CompareSitemapToHtmlParams {
|
||||
sitemapXml: string;
|
||||
baseUrl: string;
|
||||
htmlDir: string;
|
||||
postTimestampChecks?: PostTimestampCheck[];
|
||||
}
|
||||
|
||||
function normalizeUrlPath(urlPath: string): string {
|
||||
@@ -127,9 +135,46 @@ export async function compareSitemapToHtml(params: CompareSitemapToHtmlParams):
|
||||
.filter((value, index, array) => array.indexOf(value) === index)
|
||||
.sort();
|
||||
|
||||
const updatedPostPathSet = new Set<string>();
|
||||
const postTimestampChecks = Array.isArray(params.postTimestampChecks) ? params.postTimestampChecks : [];
|
||||
for (const check of postTimestampChecks) {
|
||||
const normalizedPostUrlPath = normalizeUrlPath(check.postUrlPath);
|
||||
if (!expectedPathSet.has(normalizedPostUrlPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (missingUrlPaths.includes(normalizedPostUrlPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const htmlPath = path.join(params.htmlDir, normalizedPostUrlPath === '/' ? 'index.html' : normalizedPostUrlPath.slice(1), 'index.html');
|
||||
|
||||
let htmlStat: Awaited<ReturnType<typeof fs.stat>>;
|
||||
let postStat: Awaited<ReturnType<typeof fs.stat>>;
|
||||
|
||||
try {
|
||||
htmlStat = await fs.stat(htmlPath);
|
||||
postStat = await fs.stat(check.postFilePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const generatedUpdatedAtMs = typeof check.generatedUpdatedAtMs === 'number'
|
||||
? check.generatedUpdatedAtMs
|
||||
: 0;
|
||||
const effectiveGeneratedAtMs = Math.max(htmlStat.mtimeMs, generatedUpdatedAtMs);
|
||||
|
||||
if (postStat.mtimeMs > effectiveGeneratedAtMs) {
|
||||
updatedPostPathSet.add(normalizedPostUrlPath);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPostUrlPaths = Array.from(updatedPostPathSet.values()).sort();
|
||||
|
||||
return {
|
||||
missingUrlPaths,
|
||||
extraUrlPaths,
|
||||
updatedPostUrlPaths,
|
||||
expectedUrlCount: expectedPathSet.size,
|
||||
existingHtmlUrlCount: existingHtmlPathSet.size,
|
||||
};
|
||||
|
||||
@@ -436,6 +436,7 @@ export interface SiteValidationReport {
|
||||
sitemapChanged: boolean;
|
||||
missingUrlPaths: string[];
|
||||
extraUrlPaths: string[];
|
||||
updatedPostUrlPaths: string[];
|
||||
expectedUrlCount: number;
|
||||
existingHtmlUrlCount: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user