Files
renovate/lib/modules/platform/github/api-cache.ts
T
Sergei Zharinov 2fb68b9a20 fix(github): prevent PR cache sync from exhausting all pages (#41462)
* fix(github): prevent PR cache sync from exhausting all pages

* refactor(github): derive cutoff from cached items, simplify reconcile

- When lastModified is missing but items exist (via updateItem()),
  derive cutoff from max(item.updated_at) instead of falling back
  to reconcile's return value. Eliminates the fallback branch.
- reconcile() now called purely for side effects; return value ignored.
- MAX_SYNC_PAGES guard checks needNextPageSync (not needNextPageFetch)
  to avoid spurious warning when cutoff already stopped the loop.
- Add test for mixed pages (Renovate + non-Renovate PRs interleaved).
- Update derived-cutoff test for caches without lastModified.

* fix(github): advance PR cache watermark from unfiltered page

* fix(github): add comment clarifying MAX_SYNC_PAGES is a safety net
2026-02-25 10:16:52 +00:00

88 lines
2.3 KiB
TypeScript

import { dequal } from 'dequal';
import { DateTime } from 'luxon';
import { logger } from '../../../logger/index.ts';
import type { ApiPageCache, ApiPageItem } from './types.ts';
export class ApiCache<T extends ApiPageItem> {
private cache: ApiPageCache<T>;
constructor(cache: ApiPageCache<T>) {
this.cache = cache;
}
getItems(): T[] {
return Object.values(this.cache.items);
}
getItem(number: number): T | null {
return this.cache.items[number] ?? null;
}
/**
* It intentionally doesn't alter `lastModified` cache field.
*
* The point is to allow cache modifications during run, but
* force fetching and refreshing of modified items next run.
*/
updateItem(item: T): void {
this.cache.items[item.number] = item;
}
getLastModified(): string | undefined {
return this.cache.lastModified;
}
updateLastModified(timestamp: string): void {
const current = this.cache.lastModified
? DateTime.fromISO(this.cache.lastModified)
: null;
const incoming = DateTime.fromISO(timestamp);
if (!current || incoming > current) {
this.cache.lastModified = timestamp;
}
}
/**
* Copies items from `page` to cache and updates the internal timestamp.
*
* @param page Items sorted by `updated_at` desc (most recent first).
* @returns `true` when the next page is likely to contain fresh items.
*/
reconcile(page: T[]): boolean {
if (page.length === 0) {
return false;
}
const { items } = this.cache;
let { lastModified } = this.cache;
let needNextPage = true;
for (const newItem of page) {
const number = newItem.number;
const oldItem = items[number];
const itemNewTime = DateTime.fromISO(newItem.updated_at);
const itemOldTime = oldItem?.updated_at
? DateTime.fromISO(oldItem.updated_at)
: null;
if (!dequal(oldItem, newItem)) {
logger.trace(`PR cache: updating item ${number}`);
items[number] = newItem;
}
needNextPage = itemOldTime ? itemOldTime < itemNewTime : true;
const cacheOldTime = lastModified ? DateTime.fromISO(lastModified) : null;
if (!cacheOldTime || itemNewTime > cacheOldTime) {
lastModified = newItem.updated_at;
}
}
this.cache.lastModified = lastModified;
return needNextPage;
}
}