Files
clawX/electron/gateway/prelaunch-maintenance-cache.ts
2026-05-01 21:49:35 +08:00

161 lines
4.3 KiB
TypeScript

import { app } from 'electron';
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from 'node:fs';
import { dirname, join } from 'node:path';
const CACHE_SCHEMA_VERSION = 1;
const CACHE_FILE_NAME = 'gateway-prelaunch-maintenance-cache.json';
export type PrelaunchMaintenanceTaskName =
| 'plugin-maintenance'
| 'runtime-deps-cleanup'
| 'skills-symlink-cleanup';
export interface PrelaunchMaintenanceRunResult {
executed: boolean;
reason: 'cache-hit' | 'cache-miss' | 'cache-unavailable' | 'task-failed';
}
type CacheKeyInput = string | (() => string);
type MaintenanceTask = () => void | boolean;
interface CacheEntry {
key: string;
updatedAt: string;
}
interface CacheFile {
schemaVersion: number;
tasks: Partial<Record<PrelaunchMaintenanceTaskName, CacheEntry>>;
}
function getDefaultCachePath(): string {
return join(app.getPath('userData'), CACHE_FILE_NAME);
}
function emptyCache(): CacheFile {
return {
schemaVersion: CACHE_SCHEMA_VERSION,
tasks: {},
};
}
function readCache(cachePath: string): CacheFile | null {
try {
if (!existsSync(cachePath)) return emptyCache();
const parsed = JSON.parse(readFileSync(cachePath, 'utf-8')) as CacheFile;
if (parsed.schemaVersion !== CACHE_SCHEMA_VERSION || !parsed.tasks) {
return emptyCache();
}
return parsed;
} catch {
return null;
}
}
function writeCache(cachePath: string, cache: CacheFile): boolean {
try {
mkdirSync(dirname(cachePath), { recursive: true });
writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf-8');
return true;
} catch {
return false;
}
}
export function stableJson(value: unknown): string {
if (value == null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) {
return `[${value.map((item) => stableJson(item)).join(',')}]`;
}
const entries = Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
return `{${entries.join(',')}}`;
}
export function pathSignature(path: string): string {
try {
const stat = statSync(path);
return `${stat.isDirectory() ? 'dir' : 'file'}:${Math.round(stat.mtimeMs)}:${stat.size}`;
} catch {
return 'missing';
}
}
export function directoryChildrenSignature(path: string, maxEntries = 200): string {
try {
const entries = readdirSync(path, { withFileTypes: true, encoding: 'utf8' })
.sort((left, right) => left.name.localeCompare(right.name))
.slice(0, maxEntries)
.map((entry) => {
const childPath = join(path, entry.name);
return [
entry.name,
entry.isDirectory() ? 'dir' : entry.isSymbolicLink() ? 'symlink' : 'file',
pathSignature(childPath),
].join(':');
});
return stableJson(entries);
} catch {
return 'missing';
}
}
export function buildPrelaunchMaintenanceCacheKey(parts: Record<string, unknown>): string {
return stableJson({
schemaVersion: CACHE_SCHEMA_VERSION,
...parts,
});
}
export function runCachedPrelaunchMaintenanceTask(
taskName: PrelaunchMaintenanceTaskName,
cacheKey: CacheKeyInput,
task: MaintenanceTask,
options: { cachePath?: string } = {},
): PrelaunchMaintenanceRunResult {
const readCacheKey = (): string => (typeof cacheKey === 'function' ? cacheKey() : cacheKey);
const cachePath = options.cachePath ?? getDefaultCachePath();
const cache = readCache(cachePath);
if (!cache) {
task();
return { executed: true, reason: 'cache-unavailable' };
}
let initialCacheKey: string;
try {
initialCacheKey = readCacheKey();
} catch {
task();
return { executed: true, reason: 'cache-unavailable' };
}
if (cache.tasks[taskName]?.key === initialCacheKey) {
return { executed: false, reason: 'cache-hit' };
}
const taskResult = task();
if (taskResult === false) {
return { executed: true, reason: 'task-failed' };
}
let finalCacheKey: string;
try {
finalCacheKey = readCacheKey();
} catch {
return { executed: true, reason: 'cache-unavailable' };
}
cache.tasks[taskName] = {
key: finalCacheKey,
updatedAt: new Date().toISOString(),
};
writeCache(cachePath, cache);
return { executed: true, reason: 'cache-miss' };
}