

































@@ -13,6 +13,7 @@ type ConfigClobberSnapshotFs = {
1313readdir(path: string): Promise<string[]>;
1414rmdir(path: string): Promise<unknown>;
1515stat(path: string): Promise<{ mtimeMs?: number } | null>;
16+unlink(path: string): Promise<unknown>;
1617writeFile(
1718path: string,
1819data: string,
@@ -23,6 +24,7 @@ type ConfigClobberSnapshotFs = {
2324readdirSync(path: string): string[];
2425rmdirSync(path: string): unknown;
2526statSync(path: string, options?: { throwIfNoEntry?: boolean }): { mtimeMs?: number } | null;
27+unlinkSync(path: string): unknown;
2628writeFileSync(
2729path: string,
2830data: string,
@@ -117,28 +119,59 @@ function acquireClobberLockSync(deps: ConfigClobberSnapshotDeps, lockPath: strin
117119return false;
118120}
119121120-async function countClobberedSiblings(
122+type ClobberedSiblingSnapshot = {
123+name: string;
124+path: string;
125+mtimeMs: number;
126+};
127+128+function compareClobberedSiblings(
129+left: ClobberedSiblingSnapshot,
130+right: ClobberedSiblingSnapshot,
131+): number {
132+return left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name);
133+}
134+135+async function listClobberedSiblings(
121136deps: ConfigClobberSnapshotDeps,
122137dir: string,
123138prefix: string,
124-): Promise<number> {
139+): Promise<ClobberedSiblingSnapshot[]> {
125140try {
126141const entries = await deps.fs.promises.readdir(dir);
127-return entries.filter((entry) => entry.startsWith(prefix)).length;
142+const snapshots: ClobberedSiblingSnapshot[] = [];
143+for (const entry of entries) {
144+if (!entry.startsWith(prefix)) {
145+continue;
146+}
147+const snapshotPath = path.join(dir, entry);
148+const stat = await deps.fs.promises.stat(snapshotPath).catch(() => null);
149+snapshots.push({ name: entry, path: snapshotPath, mtimeMs: stat?.mtimeMs ?? 0 });
150+}
151+return snapshots.toSorted(compareClobberedSiblings);
128152} catch {
129-return 0;
153+return [];
130154}
131155}
132156133-function countClobberedSiblingsSync(
157+function listClobberedSiblingsSync(
134158deps: ConfigClobberSnapshotDeps,
135159dir: string,
136160prefix: string,
137-): number {
161+): ClobberedSiblingSnapshot[] {
138162try {
139-return deps.fs.readdirSync(dir).filter((entry) => entry.startsWith(prefix)).length;
163+const snapshots: ClobberedSiblingSnapshot[] = [];
164+for (const entry of deps.fs.readdirSync(dir)) {
165+if (!entry.startsWith(prefix)) {
166+continue;
167+}
168+const snapshotPath = path.join(dir, entry);
169+const stat = deps.fs.statSync(snapshotPath, { throwIfNoEntry: false });
170+snapshots.push({ name: entry, path: snapshotPath, mtimeMs: stat?.mtimeMs ?? 0 });
171+}
172+return snapshots.toSorted(compareClobberedSiblings);
140173} catch {
141-return 0;
174+return [];
142175}
143176}
144177@@ -152,10 +185,44 @@ function warnClobberCapReached(
152185}
153186clobberCapWarnedPaths.add(configPath);
154187deps.logger.warn(
155-`Config clobber snapshot cap reached for ${configPath}: ${existing} existing .clobbered.* files; skipping additional forensic snapshots.`,
188+`Config clobber snapshot cap reached for ${configPath}: ${existing} existing .clobbered.* files; rotating oldest snapshots to preserve the latest forensic copy.`,
156189);
157190}
158191192+async function rotateOldestClobberedSiblings(
193+deps: ConfigClobberSnapshotDeps,
194+snapshots: ClobberedSiblingSnapshot[],
195+): Promise<boolean> {
196+const deleteCount = Math.max(0, snapshots.length - CONFIG_CLOBBER_SNAPSHOT_LIMIT + 1);
197+for (const snapshot of snapshots.slice(0, deleteCount)) {
198+try {
199+await deps.fs.promises.unlink(snapshot.path);
200+} catch (error) {
201+if (!isFsErrorCode(error, "ENOENT")) {
202+return false;
203+}
204+}
205+}
206+return true;
207+}
208+209+function rotateOldestClobberedSiblingsSync(
210+deps: ConfigClobberSnapshotDeps,
211+snapshots: ClobberedSiblingSnapshot[],
212+): boolean {
213+const deleteCount = Math.max(0, snapshots.length - CONFIG_CLOBBER_SNAPSHOT_LIMIT + 1);
214+for (const snapshot of snapshots.slice(0, deleteCount)) {
215+try {
216+deps.fs.unlinkSync(snapshot.path);
217+} catch (error) {
218+if (!isFsErrorCode(error, "ENOENT")) {
219+return false;
220+}
221+}
222+}
223+return true;
224+}
225+159226function buildClobberedTargetPath(configPath: string, observedAt: string, attempt: number): string {
160227const basePath = `${configPath}.clobbered.${formatConfigArtifactTimestamp(observedAt)}`;
161228return attempt === 0 ? basePath : `${basePath}-${String(attempt).padStart(2, "0")}`;
@@ -173,10 +240,13 @@ export async function persistBoundedClobberedConfigSnapshot(params: {
173240return null;
174241}
175242try {
176-const existing = await countClobberedSiblings(params.deps, paths.dir, paths.prefix);
177-if (existing >= CONFIG_CLOBBER_SNAPSHOT_LIMIT) {
178-warnClobberCapReached(params.deps, params.configPath, existing);
179-return null;
243+const existing = await listClobberedSiblings(params.deps, paths.dir, paths.prefix);
244+if (existing.length >= CONFIG_CLOBBER_SNAPSHOT_LIMIT) {
245+warnClobberCapReached(params.deps, params.configPath, existing.length);
246+const rotated = await rotateOldestClobberedSiblings(params.deps, existing);
247+if (!rotated) {
248+return null;
249+}
180250}
181251for (let attempt = 0; attempt < CONFIG_CLOBBER_SNAPSHOT_LIMIT; attempt++) {
182252const targetPath = buildClobberedTargetPath(params.configPath, params.observedAt, attempt);
@@ -210,10 +280,12 @@ export function persistBoundedClobberedConfigSnapshotSync(params: {
210280return null;
211281}
212282try {
213-const existing = countClobberedSiblingsSync(params.deps, paths.dir, paths.prefix);
214-if (existing >= CONFIG_CLOBBER_SNAPSHOT_LIMIT) {
215-warnClobberCapReached(params.deps, params.configPath, existing);
216-return null;
283+const existing = listClobberedSiblingsSync(params.deps, paths.dir, paths.prefix);
284+if (existing.length >= CONFIG_CLOBBER_SNAPSHOT_LIMIT) {
285+warnClobberCapReached(params.deps, params.configPath, existing.length);
286+if (!rotateOldestClobberedSiblingsSync(params.deps, existing)) {
287+return null;
288+}
217289}
218290for (let attempt = 0; attempt < CONFIG_CLOBBER_SNAPSHOT_LIMIT; attempt++) {
219291const targetPath = buildClobberedTargetPath(params.configPath, params.observedAt, attempt);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。