From 299fb52774ed153d5baf9db1cbbd44143546fdd0 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:31:37 +0200 Subject: [PATCH] feat(RecommendationsRegistry): uses deterministic method to choose which banner is displayed (#6947) feat(RecommendationsRegistry): uses deterministic to provide list of extension banners Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- .../recommendations-registry.spec.ts | 79 +++++++++++++++++++ .../recommendations-registry.ts | 42 +++++----- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/packages/main/src/plugin/recommendations/recommendations-registry.spec.ts b/packages/main/src/plugin/recommendations/recommendations-registry.spec.ts index 6d84c2e2550..c1493e75020 100644 --- a/packages/main/src/plugin/recommendations/recommendations-registry.spec.ts +++ b/packages/main/src/plugin/recommendations/recommendations-registry.spec.ts @@ -248,6 +248,85 @@ describe('getExtensionBanners', () => { expect(featuredMock.getFeaturedExtensions).toHaveBeenCalled(); }); + + test('same time should return same arrays', async () => { + getRecommendationIgnored.mockReturnValue(false); + vi.mocked(featuredMock.getFeaturedExtensions).mockResolvedValue( + Array.from({ length: 10 }, (_, i) => ({ + id: `dummy.id-${i}`, + builtin: false, + description: '', + categories: [], + displayName: '', + fetchable: false, + icon: '', + installed: false, + })), + ); + + vi.setSystemTime(new Date(2050, 1, 1, 1)); + const base = await recommendationsRegistry.getExtensionBanners(5); + + for (let i = 0; i < 10; i++) { + expect(base).toStrictEqual(await recommendationsRegistry.getExtensionBanners(5)); + } + }); + + test('different hours should return different arrays', async () => { + getRecommendationIgnored.mockReturnValue(false); + vi.mocked(featuredMock.getFeaturedExtensions).mockResolvedValue( + Array.from({ length: 10 }, (_, i) => ({ + id: `dummy.id-${i}`, + builtin: false, + description: '', + categories: [], + displayName: '', + fetchable: false, + icon: '', + installed: false, + })), + ); + + vi.setSystemTime(new Date(2050, 1, 1, 1)); + const resultA = await recommendationsRegistry.getExtensionBanners(5); + expect(resultA.length).toBe(5); + + vi.setSystemTime(new Date(2050, 1, 1, 2)); + const resultB = await recommendationsRegistry.getExtensionBanners(5); + expect(resultB.length).toBe(5); + + expect(resultA).not.toStrictEqual(resultB); + }); + + test('all elements should have been shown in one day', async () => { + getRecommendationIgnored.mockReturnValue(false); + const featured = Array.from({ length: 10 }, (_, i) => ({ + id: `dummy.id-${i}`, + builtin: false, + description: '', + categories: [], + displayName: '', + fetchable: false, + icon: '', + installed: false, + })); + vi.mocked(featuredMock.getFeaturedExtensions).mockResolvedValue(featured); + + const expectedIds: Set = new Set(featured.map(item => item.id)); + + const actualsIds: Set = new Set(); + + for (let h = 0; h < 24; h++) { + vi.setSystemTime(new Date(2050, 1, 1, h)); + + const banners = await recommendationsRegistry.getExtensionBanners(1); + expect(banners.length).toBe(1); + + actualsIds.add(banners[0].extensionId); + } + + expect(expectedIds).toStrictEqual(actualsIds); + }); }); describe('getRegistries', () => { diff --git a/packages/main/src/plugin/recommendations/recommendations-registry.ts b/packages/main/src/plugin/recommendations/recommendations-registry.ts index 494a4aaf0dd..653a7de82b0 100644 --- a/packages/main/src/plugin/recommendations/recommendations-registry.ts +++ b/packages/main/src/plugin/recommendations/recommendations-registry.ts @@ -93,33 +93,35 @@ export class RecommendationsRegistry { ); // Filter and shuffle the extensions - const extensionBanners: ExtensionBanner[] = recommendations.extensions - .reduce((prev, extension) => { - // ensure the extension is in the featured extensions and is not install - if (!(extension.extensionId in featuredExtensions) || featuredExtensions[extension.extensionId].installed) { + const extensionBanners: ExtensionBanner[] = recommendations.extensions.reduce((prev, extension) => { + // ensure the extension is in the featured extensions and is not install + if (!(extension.extensionId in featuredExtensions) || featuredExtensions[extension.extensionId].installed) { + return prev; + } + + // Check for publishDate property + if ('publishDate' in extension && typeof extension.publishDate === 'string') { + const publishDate = new Date(extension.publishDate).getTime(); + if (isNaN(publishDate) || publishDate > Date.now()) { return prev; } + } - // Check for publishDate property - if ('publishDate' in extension && typeof extension.publishDate === 'string') { - const publishDate = new Date(extension.publishDate).getTime(); - if (isNaN(publishDate) || publishDate > Date.now()) { - return prev; - } - } + prev.push({ + ...extension, + featured: featuredExtensions[extension.extensionId], + }); - prev.push({ - ...extension, - featured: featuredExtensions[extension.extensionId], - }); - - return prev; - }, [] as ExtensionBanner[]) - .toSorted(() => Math.random() - 0.5); + return prev; + }, [] as ExtensionBanner[]); // Limit the number of if (limit >= 0 && extensionBanners.length > limit) { - return extensionBanners.toSpliced(limit); + // instead of using random generator we ensure deterministic results for a period of time (here by the hours) + const startingIndex = new Date().getHours() % extensionBanners.length; + + // Let's return the subset of banners starting at the chosen index + return Array.from({ length: limit }, (_, i) => extensionBanners[(startingIndex + i) % extensionBanners.length]); } return extensionBanners; }