import { dequal } from 'dequal'; import { DateTime } from 'luxon'; import * as packageCache from '../../../util/cache/package/index.ts'; import type { DockerHubTag } from './schema.ts'; import type { DockerHubCacheData } from './types.ts'; const cacheNamespace = 'datasource-docker-hub-cache'; export class DockerHubCache { private isChanged = false; private reconciledIds = new Set(); private dockerRepository: string; private cache: DockerHubCacheData; private constructor(dockerRepository: string, cache: DockerHubCacheData) { this.dockerRepository = dockerRepository; this.cache = cache; } static async init(dockerRepository: string): Promise { let repoCache = await packageCache.get( cacheNamespace, dockerRepository, ); repoCache ??= { items: {}, updatedAt: null, }; return new DockerHubCache(dockerRepository, repoCache); } reconcile(items: DockerHubTag[], expectedCount: number): boolean { let needNextPage = true; let earliestDate = null; let { updatedAt } = this.cache; let latestDate = updatedAt ? DateTime.fromISO(updatedAt) : null; for (const newItem of items) { const id = newItem.id; this.reconciledIds.add(id); const oldItem = this.cache.items[id]; const itemDate = DateTime.fromISO(newItem.last_updated); if (!earliestDate || earliestDate > itemDate) { earliestDate = itemDate; } if (!latestDate || latestDate < itemDate) { latestDate = itemDate; updatedAt = newItem.last_updated; } if (dequal(oldItem, newItem)) { needNextPage = false; continue; } this.cache.items[newItem.id] = newItem; this.isChanged = true; } this.cache.updatedAt = updatedAt; // v8 ignore else -- needs a cache populated across two sync runs if (earliestDate && latestDate) { for (const [key, item] of Object.entries(this.cache.items)) { const id = parseInt(key, 10); const itemDate = DateTime.fromISO(item.last_updated); if ( itemDate < earliestDate || itemDate > latestDate || this.reconciledIds.has(id) ) { continue; } delete this.cache.items[id]; this.isChanged = true; } if (Object.keys(this.cache.items).length > expectedCount) { return true; } } return needNextPage; } async save(): Promise { if (this.isChanged) { await packageCache.set( cacheNamespace, this.dockerRepository, this.cache, 3 * 60 * 24 * 30, ); } } getItems(): DockerHubTag[] { return Object.values(this.cache.items); } getDigestForTag(tagName: string): string | null { return ( this.getItems().find((item) => item.name === tagName)?.digest ?? null ); } getArchDigestForTag(tagName: string, architecture: string): string | null { const tag = this.getItems().find((item) => item.name === tagName); return ( tag?.images.find((img) => img.architecture === architecture)?.digest ?? null ); } }