From 6698c206038b6b20e53f969fdac4bff3793400d9 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov <zharinov@users.noreply.github.com> Date: Fri, 8 Apr 2022 08:54:17 +0300 Subject: [PATCH] feat(github): Mapping functions for ApiCache items (#15010) --- lib/modules/platform/github/api-cache.spec.ts | 108 ++++++++++++++++++ lib/modules/platform/github/api-cache.ts | 28 ++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/github/api-cache.spec.ts b/lib/modules/platform/github/api-cache.spec.ts index d0c554c585..72cefeca8c 100644 --- a/lib/modules/platform/github/api-cache.spec.ts +++ b/lib/modules/platform/github/api-cache.spec.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; import { ApiCache } from './api-cache'; +import type { ApiPageItem } from './types'; describe('modules/platform/github/api-cache', () => { const now = DateTime.fromISO('2000-01-01T00:00:00.000+00:00'); @@ -35,6 +36,113 @@ describe('modules/platform/github/api-cache', () => { expect(apiCache.getItems()).toEqual([item1, item2]); }); + describe('getItems', () => { + it('maps items', () => { + const item1 = { number: 1, updated_at: t1 }; + const item2 = { number: 2, updated_at: t2 }; + const item3 = { number: 3, updated_at: t3 }; + const apiCache = new ApiCache({ + items: { + 1: item1, + 2: item2, + 3: item3, + }, + }); + + const res = apiCache.getItems(({ number }) => number); + + expect(res).toEqual([1, 2, 3]); + }); + + it('caches mapping results', () => { + const item1 = { number: 1, updated_at: t1 }; + const item2 = { number: 2, updated_at: t2 }; + const item3 = { number: 3, updated_at: t3 }; + const apiCache = new ApiCache({ + items: { + 1: item1, + 2: item2, + 3: item3, + }, + }); + + let numbersMapCalls = 0; + const mapNumbers = ({ number }: ApiPageItem) => { + numbersMapCalls += 1; + return number; + }; + + let datesMapCalls = 0; + const mapDates = ({ updated_at }: ApiPageItem) => { + datesMapCalls += 1; + return updated_at; + }; + + const numbers1 = apiCache.getItems(mapNumbers); + const numbers2 = apiCache.getItems(mapNumbers); + const dates1 = apiCache.getItems(mapDates); + const dates2 = apiCache.getItems(mapDates); + + expect(numbers1).toEqual([1, 2, 3]); + expect(numbers1).toBe(numbers2); + expect(numbersMapCalls).toBe(3); + + expect(dates1).toEqual([t1, t2, t3]); + expect(dates1).toBe(dates2); + expect(datesMapCalls).toBe(3); + }); + + it('resets cache on item update', () => { + const item1 = { number: 1, updated_at: t1 }; + const item2 = { number: 2, updated_at: t2 }; + const item3 = { number: 3, updated_at: t3 }; + const apiCache = new ApiCache({ + items: { + 1: item1, + 2: item2, + }, + }); + + let numbersMapCalls = 0; + const mapNumbers = ({ number }: ApiPageItem) => { + numbersMapCalls += 1; + return number; + }; + const numbers1 = apiCache.getItems(mapNumbers); + apiCache.updateItem(item3); + const numbers2 = apiCache.getItems(mapNumbers); + + expect(numbers1).toEqual([1, 2]); + expect(numbers2).toEqual([1, 2, 3]); + expect(numbersMapCalls).toBe(5); + }); + + it('resets cache on page reconcile', () => { + const item1 = { number: 1, updated_at: t1 }; + const item2 = { number: 2, updated_at: t2 }; + const item3 = { number: 3, updated_at: t3 }; + const apiCache = new ApiCache({ + items: { + 1: item1, + 2: item2, + }, + }); + + let numbersMapCalls = 0; + const mapNumbers = ({ number }: ApiPageItem) => { + numbersMapCalls += 1; + return number; + }; + const numbers1 = apiCache.getItems(mapNumbers); + apiCache.reconcile([item3]); + const numbers2 = apiCache.getItems(mapNumbers); + + expect(numbers1).toEqual([1, 2]); + expect(numbers2).toEqual([1, 2, 3]); + expect(numbersMapCalls).toBe(5); + }); + }); + describe('reconcile', () => { it('appends new items', () => { const apiCache = new ApiCache({ items: {} }); diff --git a/lib/modules/platform/github/api-cache.ts b/lib/modules/platform/github/api-cache.ts index 09d4fed91c..279b5c7b32 100644 --- a/lib/modules/platform/github/api-cache.ts +++ b/lib/modules/platform/github/api-cache.ts @@ -1,7 +1,10 @@ +import { dequal } from 'dequal'; import { DateTime } from 'luxon'; import type { ApiPageCache, ApiPageItem } from './types'; export class ApiCache<T extends ApiPageItem> { + private itemsMapCache = new WeakMap(); + constructor(private cache: ApiPageCache<T>) {} get etag(): string | null { @@ -24,8 +27,23 @@ export class ApiCache<T extends ApiPageItem> { return lastModified ? DateTime.fromISO(lastModified).toHTTP() : null; } - getItems(): T[] { - return Object.values(this.cache.items); + getItems(): T[]; + getItems<U = unknown>(mapFn: (_: T) => U): U[]; + getItems<U = unknown>(mapFn?: (_: T) => U): T[] | U[] { + if (mapFn) { + const cachedResult = this.itemsMapCache.get(mapFn); + if (cachedResult) { + return cachedResult; + } + + const items = Object.values(this.cache.items); + const mappedResult = items.map(mapFn); + this.itemsMapCache.set(mapFn, mappedResult); + return mappedResult; + } + + const items = Object.values(this.cache.items); + return items; } getItem(number: number): T | null { @@ -40,6 +58,7 @@ export class ApiCache<T extends ApiPageItem> { */ updateItem(item: T): void { this.cache.items[item.number] = item; + this.itemsMapCache = new WeakMap(); } /** @@ -67,7 +86,10 @@ export class ApiCache<T extends ApiPageItem> { ? DateTime.fromISO(oldItem.updated_at) : null; - items[number] = newItem; + if (!dequal(oldItem, newItem)) { + items[number] = newItem; + this.itemsMapCache = new WeakMap(); + } needNextPage = itemOldTime ? itemOldTime < itemNewTime : true; -- GitLab