feat: 0.9.0 忽略规则、增量缓存跨会话持久化、快照基线、字符级预算自适应与性能基准E2E - 忽略规则(gitignore简化子集:目录整树剪枝不枚举不计数、扩展名通配与名字匹配、随偏好持久化)、扫描增量缓存落盘userData跨会话自动注入(LRU 8对、防御性解析、渲染端零改动)、快照基线(保存/加载JSON对照六类状态变迁统计与条目标注、对照随重扫刷新、报告附快照口径)、字符级变更组预算随输入规模自适应(下限与固定值一致小输入零变化、大输入按总组流半数放宽上限100万封顶)、大文件性能基准E2E(5万行<15s与300KB GBK<10s耗时断言)+新功能E2E、文档与版本号同步
This commit is contained in:
+55
-5
@@ -6,6 +6,14 @@ import fs from 'fs'
|
||||
import { parseState, clampBounds, type AppState } from './windowState'
|
||||
import { decodeText, type DecodeResult } from './decode'
|
||||
import { scanFolders, SEMANTIC_MAX_BYTES, type ScanCacheEntry } from './folderScan'
|
||||
import { isIgnoreRulesArray } from './ignoreRules'
|
||||
import {
|
||||
parseScanCacheStore,
|
||||
putScanCache,
|
||||
getScanCache,
|
||||
emptyScanCacheStore,
|
||||
type ScanCacheStoreData
|
||||
} from './scanCacheStore'
|
||||
|
||||
/**
|
||||
* 文本类扩展名清单(小写,不含点):
|
||||
@@ -30,6 +38,35 @@ function isTextRel(rel: string): boolean {
|
||||
/** 应用状态持久化文件(窗口 bounds + 上次文件目录),位于 userData 目录 */
|
||||
const stateFile = (): string => join(app.getPath('userData'), 'window-state.json')
|
||||
|
||||
/** 文件夹扫描增量缓存的跨会话持久化文件,位于 userData 目录 */
|
||||
const scanCacheFile = (): string => join(app.getPath('userData'), 'scan-cache.json')
|
||||
|
||||
/** 读取持久化扫描缓存存储:文件不存在/损坏回退空存储,不影响扫描 */
|
||||
function loadScanCacheStore(): ScanCacheStoreData {
|
||||
try {
|
||||
return parseScanCacheStore(fs.readFileSync(scanCacheFile(), 'utf-8'))
|
||||
} catch {
|
||||
return emptyScanCacheStore()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 落盘扫描缓存(读-改-写,异步 fire-and-forget):
|
||||
* 失败静默(目录只读/磁盘满等,跨会话增量记忆失效不影响本次结果)。
|
||||
*/
|
||||
async function persistScanCache(
|
||||
leftDir: string,
|
||||
rightDir: string,
|
||||
cache: ScanCacheEntry[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const next = putScanCache(loadScanCacheStore(), leftDir, rightDir, cache, Date.now())
|
||||
await fs.promises.writeFile(scanCacheFile(), JSON.stringify(next), 'utf-8')
|
||||
} catch {
|
||||
// 忽略写入失败
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前生效的应用状态(启动时从磁盘读取,运行中增量更新) */
|
||||
let appState: AppState = {}
|
||||
|
||||
@@ -264,7 +301,7 @@ ipcMain.handle('folder:pick', async () => {
|
||||
return dirPath
|
||||
})
|
||||
|
||||
/** IPC: 扫描对比两个文件夹(递归对齐 + 字节级判定 + 可选语义判等 + 增量缓存;异步 fs 与解码 worker 不阻塞主进程) */
|
||||
/** IPC: 扫描对比两个文件夹(递归对齐 + 字节级判定 + 可选语义判等 + 忽略规则 + 增量缓存跨会话持久化;异步 fs 与解码 worker 不阻塞主进程) */
|
||||
ipcMain.handle(
|
||||
'folder:scan',
|
||||
async (
|
||||
@@ -272,10 +309,11 @@ ipcMain.handle(
|
||||
leftDir: string,
|
||||
rightDir: string,
|
||||
semantic: boolean,
|
||||
previous?: unknown
|
||||
previous?: unknown,
|
||||
ignoreRules?: unknown
|
||||
) => {
|
||||
try {
|
||||
// 增量缓存防御性校验:仅接受数组形态(渲染端从上次扫描结果携带,损坏即全量重扫)
|
||||
// 增量缓存防御性校验:仅接受数组形态(渲染端从上次扫描结果携带,损坏即不沿用)
|
||||
const cache =
|
||||
Array.isArray(previous) && previous.every(
|
||||
(c) =>
|
||||
@@ -286,8 +324,17 @@ ipcMain.handle(
|
||||
)
|
||||
? (previous as ScanCacheEntry[])
|
||||
: undefined
|
||||
return await scanFolders(leftDir, rightDir, {
|
||||
...(cache ? { previous: cache } : {}),
|
||||
// 渲染端未携带内存缓存时(跨会话首次扫描),尝试注入 userData 落盘的持久化缓存
|
||||
let prev = cache
|
||||
if (prev === undefined) {
|
||||
const stored = getScanCache(loadScanCacheStore(), leftDir, rightDir)
|
||||
if (stored !== null && stored.length > 0) prev = stored
|
||||
}
|
||||
// 忽略规则防御性校验:仅接受字符串数组形态(渲染端从偏好携带,损坏即忽略)
|
||||
const ignore = isIgnoreRulesArray(ignoreRules) ? ignoreRules : undefined
|
||||
const result = await scanFolders(leftDir, rightDir, {
|
||||
...(prev ? { previous: prev } : {}),
|
||||
...(ignore ? { ignore } : {}),
|
||||
...(semantic
|
||||
? {
|
||||
// 语义判等解码复用解码 worker(大缓冲后台解码,与文件打开共用同一管线)
|
||||
@@ -299,6 +346,9 @@ ipcMain.handle(
|
||||
}
|
||||
: {})
|
||||
})
|
||||
// 扫描成功且未截断:缓存异步落盘(LRU,失败静默;下次启动同目录对可增量重扫)
|
||||
if (!result.truncated) void persistScanCache(leftDir, rightDir, result.cache)
|
||||
return result
|
||||
} catch {
|
||||
// 目录不存在/不可读等:转为错误标记,渲染端提示
|
||||
return { error: 'scan-failed' as const }
|
||||
|
||||
Reference in New Issue
Block a user