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