diff --git a/src/__data__/hotSceneGenesisPlaza.ts b/src/__data__/hotSceneGenesisPlaza.ts index 2a78a641..1f58bda8 100644 --- a/src/__data__/hotSceneGenesisPlaza.ts +++ b/src/__data__/hotSceneGenesisPlaza.ts @@ -1,10 +1,10 @@ import { HotScene } from "../entities/Place/types" -export const hotSceneGenesisPlaza: HotScene = { +export const hotSceneGenesisPlazaLegacy: HotScene = { id: "bafkreidw4inuymukjj4otmld76a5qo4sowc6lbqqk6h4dtci4yxv5qkjie", name: "Genesis Plaza", baseCoords: [-9, -9], - usersTotalCount: 25, + usersTotalCount: 11, parcels: [ [-9, -9], [-9, -8], @@ -394,3 +394,393 @@ export const hotSceneGenesisPlaza: HotScene = { }, ], } + +export const hotSceneGenesisPlaza: HotScene = { + id: "bafkreidw4inuymukjj4otmld76a5qo4sowc6lbqqk6h4dtci4yxv5qkjie", + name: "Genesis Plaza", + baseCoords: [-9, -9], + usersTotalCount: 10, + parcels: [ + [-9, -9], + [-9, -8], + [-9, -7], + [-9, -6], + [-9, -5], + [-9, -4], + [-9, -3], + [-9, -2], + [-9, -1], + [-9, 0], + [-9, 1], + [-9, 2], + [-9, 3], + [-9, 4], + [-9, 5], + [-9, 6], + [-9, 7], + [-9, 8], + [-9, 9], + [-8, -9], + [-8, -8], + [-8, -7], + [-8, -6], + [-8, -5], + [-8, -4], + [-8, -3], + [-8, -2], + [-8, -1], + [-8, 0], + [-8, 1], + [-8, 2], + [-8, 3], + [-8, 4], + [-8, 5], + [-8, 6], + [-8, 7], + [-8, 8], + [-8, 9], + [-7, -9], + [-7, -8], + [-7, -7], + [-7, -6], + [-7, -5], + [-7, -4], + [-7, -3], + [-7, -2], + [-7, -1], + [-7, 0], + [-7, 1], + [-7, 2], + [-7, 3], + [-7, 4], + [-7, 5], + [-7, 6], + [-7, 7], + [-7, 8], + [-7, 9], + [-6, -9], + [-6, -8], + [-6, -7], + [-6, -6], + [-6, -5], + [-6, -4], + [-6, -3], + [-6, -2], + [-6, -1], + [-6, 0], + [-6, 1], + [-6, 2], + [-6, 3], + [-6, 4], + [-6, 5], + [-6, 6], + [-6, 7], + [-6, 8], + [-6, 9], + [-5, -9], + [-5, -8], + [-5, -7], + [-5, -6], + [-5, -5], + [-5, -4], + [-5, -3], + [-5, -2], + [-5, -1], + [-5, 0], + [-5, 1], + [-5, 2], + [-5, 3], + [-5, 4], + [-5, 5], + [-5, 6], + [-5, 7], + [-5, 8], + [-5, 9], + [-4, -9], + [-4, -8], + [-4, -7], + [-4, -6], + [-4, -5], + [-4, -4], + [-4, -3], + [-4, -2], + [-4, -1], + [-4, 0], + [-4, 1], + [-4, 2], + [-4, 3], + [-4, 4], + [-4, 5], + [-4, 6], + [-4, 7], + [-4, 8], + [-4, 9], + [-3, -9], + [-3, -8], + [-3, -7], + [-3, -6], + [-3, -5], + [-3, -4], + [-3, -3], + [-3, -2], + [-3, -1], + [-3, 0], + [-3, 1], + [-3, 2], + [-3, 3], + [-3, 4], + [-3, 5], + [-3, 6], + [-3, 7], + [-3, 8], + [-3, 9], + [-2, -9], + [-2, -8], + [-2, -7], + [-2, -6], + [-2, -5], + [-2, -4], + [-2, -3], + [-2, -2], + [-2, -1], + [-2, 0], + [-2, 1], + [-2, 2], + [-2, 3], + [-2, 4], + [-2, 5], + [-2, 6], + [-2, 7], + [-2, 8], + [-2, 9], + [-1, -9], + [-1, -8], + [-1, -7], + [-1, -6], + [-1, -5], + [-1, -4], + [-1, -3], + [-1, -2], + [-1, -1], + [-1, 0], + [-1, 1], + [-1, 2], + [-1, 3], + [-1, 4], + [-1, 5], + [-1, 6], + [-1, 7], + [-1, 8], + [-1, 9], + [0, -9], + [0, -8], + [0, -7], + [0, -6], + [0, -5], + [0, -4], + [0, -3], + [0, -2], + [0, -1], + [0, 0], + [0, 1], + [0, 2], + [0, 3], + [0, 4], + [0, 5], + [0, 6], + [0, 7], + [0, 8], + [0, 9], + [1, -9], + [1, -8], + [1, -7], + [1, -6], + [1, -5], + [1, -4], + [1, -3], + [1, -2], + [1, -1], + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [1, 4], + [1, 5], + [1, 6], + [1, 7], + [1, 8], + [1, 9], + [2, -9], + [2, -8], + [2, -7], + [2, -6], + [2, -5], + [2, -4], + [2, -3], + [2, -2], + [2, -1], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + [2, 4], + [2, 5], + [2, 6], + [2, 7], + [2, 8], + [2, 9], + [3, -9], + [3, -8], + [3, -7], + [3, -6], + [3, -5], + [3, -4], + [3, -3], + [3, -2], + [3, -1], + [3, 0], + [3, 1], + [3, 2], + [3, 3], + [3, 4], + [3, 5], + [3, 6], + [3, 7], + [3, 8], + [3, 9], + [4, -9], + [4, -8], + [4, -7], + [4, -6], + [4, -5], + [4, -4], + [4, -3], + [4, -2], + [4, -1], + [4, 0], + [4, 1], + [4, 2], + [4, 3], + [4, 4], + [4, 5], + [4, 6], + [4, 7], + [4, 8], + [4, 9], + [5, -9], + [5, -8], + [5, -7], + [5, -6], + [5, -5], + [5, -4], + [5, -3], + [5, -2], + [5, -1], + [5, 0], + [5, 1], + [5, 2], + [5, 3], + [5, 4], + [5, 5], + [5, 6], + [5, 7], + [5, 8], + [5, 9], + [6, -9], + [6, -8], + [6, -7], + [6, -6], + [6, -5], + [6, -4], + [6, -3], + [6, -2], + [6, -1], + [6, 0], + [6, 1], + [6, 2], + [6, 3], + [6, 4], + [6, 5], + [6, 6], + [6, 7], + [6, 8], + [6, 9], + [7, -9], + [7, -8], + [7, -7], + [7, -6], + [7, -5], + [7, -4], + [7, -3], + [7, -2], + [7, -1], + [7, 0], + [7, 1], + [7, 2], + [7, 3], + [7, 4], + [7, 5], + [7, 6], + [7, 7], + [7, 8], + [7, 9], + [8, -9], + [8, -8], + [8, -7], + [8, -6], + [8, -5], + [8, -4], + [8, -3], + [8, -2], + [8, -1], + [8, 0], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [8, 6], + [8, 7], + [8, 8], + [8, 9], + [9, -9], + [9, -8], + [9, -7], + [9, -6], + [9, -5], + [9, -4], + [9, -3], + [9, -2], + [9, -1], + [9, 0], + [9, 1], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 6], + [9, 7], + [9, 8], + [9, 9], + [10, -9], + [10, -8], + [10, -7], + [10, -6], + [10, -5], + [10, -4], + [10, -3], + [10, -2], + [10, -1], + [10, 0], + [10, 1], + [10, 2], + [10, 3], + [10, 4], + [10, 5], + [10, 6], + [10, 7], + [10, 8], + [10, 9], + ], + realms: [], +} diff --git a/src/entities/Place/model.ts b/src/entities/Place/model.ts index 5518a62b..f70a5e22 100644 --- a/src/entities/Place/model.ts +++ b/src/entities/Place/model.ts @@ -161,9 +161,7 @@ export default class PlaceModel extends Model { ) const filterMostActivePlaces = - options.order_by === PlaceListOrderBy.MOST_ACTIVE && - !!options.hotScenesPositions && - options.hotScenesPositions.length > 0 + !!options.hotScenesPositions && options.hotScenesPositions.length > 0 const sql = SQL` ${conditional( diff --git a/src/entities/Place/routes/getPlace.test.ts b/src/entities/Place/routes/getPlace.test.ts index 9d469279..443d0519 100644 --- a/src/entities/Place/routes/getPlace.test.ts +++ b/src/entities/Place/routes/getPlace.test.ts @@ -5,9 +5,9 @@ import { Request } from "decentraland-gatsby/dist/entities/Route/wkc/request/Req import { hotSceneGenesisPlaza } from "../../../__data__/hotSceneGenesisPlaza" import { placeGenesisPlazaWithAggregatedAttributes } from "../../../__data__/placeGenesisPlazaWithAggregatedAttributes" import { sceneStatsGenesisPlaza } from "../../../__data__/sceneStatsGenesisPlaza" -import DataTeam from "../../../api/DataTeam" -import * as hotScenesModule from "../../../modules/hotScenes" import PlaceCategories from "../../PlaceCategories/model" +import * as hotScenesModule from "../../RealmProvider/utils" +import * as sceneStatsModule from "../../SceneStats/utils" import PlaceModel from "../model" import { getPlace } from "./getPlace" @@ -15,11 +15,12 @@ const place_id = randomUUID() const findOne = jest.spyOn(PlaceModel, "namedQuery") const catalystHotScenes = jest.spyOn(hotScenesModule, "getHotScenes") const findPC = jest.spyOn(PlaceCategories, "namedQuery") -const catalystSceneStats = jest.spyOn(DataTeam.get(), "getSceneStats") +const catalystSceneStats = jest.spyOn(sceneStatsModule, "getSceneStats") afterEach(() => { findOne.mockReset() catalystHotScenes.mockReset() + catalystSceneStats.mockReset() findPC.mockReset() }) test("should return 400 when UUID is incorrect", async () => { diff --git a/src/entities/Place/routes/getPlace.ts b/src/entities/Place/routes/getPlace.ts index 5d5fd81c..215bf704 100644 --- a/src/entities/Place/routes/getPlace.ts +++ b/src/entities/Place/routes/getPlace.ts @@ -6,8 +6,8 @@ import Response from "decentraland-gatsby/dist/entities/Route/wkc/response/Respo import Router from "decentraland-gatsby/dist/entities/Route/wkc/routes/Router" import { bool } from "decentraland-gatsby/dist/entities/Schema/utils" -import { getHotScenes } from "../../../modules/hotScenes" -import { getSceneStats } from "../../../modules/sceneStats" +import { getHotScenes } from "../../RealmProvider/utils" +import { getSceneStats } from "../../SceneStats/utils" import PlaceModel from "../model" import { getPlaceParamsSchema } from "../schemas" import { AggregatePlaceAttributes, GetPlaceParams } from "../types" diff --git a/src/entities/Place/routes/getPlaceList.test.ts b/src/entities/Place/routes/getPlaceList.test.ts index 5b96d1e6..230c67db 100644 --- a/src/entities/Place/routes/getPlaceList.test.ts +++ b/src/entities/Place/routes/getPlaceList.test.ts @@ -3,18 +3,19 @@ import { Request } from "decentraland-gatsby/dist/entities/Route/wkc/request/Req import { hotSceneGenesisPlaza } from "../../../__data__/hotSceneGenesisPlaza" import { placeGenesisPlazaWithAggregatedAttributes } from "../../../__data__/placeGenesisPlazaWithAggregatedAttributes" import { sceneStatsGenesisPlaza } from "../../../__data__/sceneStatsGenesisPlaza" -import DataTeam from "../../../api/DataTeam" -import * as hotScenesModule from "../../../modules/hotScenes" +import * as hotScenesModule from "../../RealmProvider/utils" +import * as sceneStatsModule from "../../SceneStats/utils" import PlaceModel from "../model" import { getPlaceList } from "./getPlaceList" const find = jest.spyOn(PlaceModel, "namedQuery") const catalystHotScenes = jest.spyOn(hotScenesModule, "getHotScenes") -const catalystSceneStats = jest.spyOn(DataTeam.get(), "getSceneStats") +const catalystSceneStats = jest.spyOn(sceneStatsModule, "getSceneStats") afterEach(() => { find.mockReset() catalystHotScenes.mockReset() + catalystSceneStats.mockReset() }) test("should return a list of places with no query", async () => { diff --git a/src/entities/Place/routes/getPlaceList.ts b/src/entities/Place/routes/getPlaceList.ts index d8a807db..cb558b53 100644 --- a/src/entities/Place/routes/getPlaceList.ts +++ b/src/entities/Place/routes/getPlaceList.ts @@ -8,8 +8,8 @@ import { oneOf, } from "decentraland-gatsby/dist/entities/Schema/utils" -import { getHotScenes } from "../../../modules/hotScenes" -import { getSceneStats } from "../../../modules/sceneStats" +import { getHotScenes } from "../../RealmProvider/utils" +import { getSceneStats } from "../../SceneStats/utils" import PlaceModel from "../model" import { getPlaceListQuerySchema } from "../schemas" import { diff --git a/src/entities/Place/routes/getPlaceMostActiveList.ts b/src/entities/Place/routes/getPlaceMostActiveList.ts index 4a3eb2d0..c24be560 100644 --- a/src/entities/Place/routes/getPlaceMostActiveList.ts +++ b/src/entities/Place/routes/getPlaceMostActiveList.ts @@ -9,8 +9,8 @@ import { } from "decentraland-gatsby/dist/entities/Schema/utils" import { flat, sort } from "radash" -import { getHotScenes } from "../../../modules/hotScenes" -import { getSceneStats } from "../../../modules/sceneStats" +import { getHotScenes } from "../../RealmProvider/utils" +import { getSceneStats } from "../../SceneStats/utils" import PlaceModel from "../model" import { FindWithAggregatesOptions, PlaceListOrderBy } from "../types" import { placesWithUserCount, placesWithUserVisits } from "../utils" @@ -60,7 +60,7 @@ export const getPlaceMostActiveList = Router.memo( only_highlighted: !!bool(query.only_highlighted), positions: query.positions, hotScenesPositions: hotScenesPositions, - order_by: PlaceListOrderBy.MOST_ACTIVE, + order_by: PlaceListOrderBy.UPDATED_AT, order: query.order, search: query.search, categories: query.categories, diff --git a/src/entities/Place/routes/getPlaceUserVisitsList.ts b/src/entities/Place/routes/getPlaceUserVisitsList.ts index 182fb091..dbb9d50a 100644 --- a/src/entities/Place/routes/getPlaceUserVisitsList.ts +++ b/src/entities/Place/routes/getPlaceUserVisitsList.ts @@ -5,8 +5,8 @@ import Router from "decentraland-gatsby/dist/entities/Route/wkc/routes/Router" import { bool, numeric } from "decentraland-gatsby/dist/entities/Schema/utils" import { sort } from "radash" -import { getHotScenes } from "../../../modules/hotScenes" -import { getSceneStats } from "../../../modules/sceneStats" +import { getHotScenes } from "../../RealmProvider/utils" +import { getSceneStats } from "../../SceneStats/utils" import PlaceModel from "../model" import { FindWithAggregatesOptions, PlaceListOrderBy } from "../types" import { placesWithUserCount, placesWithUserVisits } from "../utils" diff --git a/src/entities/RealmProvider/utils.test.ts b/src/entities/RealmProvider/utils.test.ts index 4e4eaba8..25ac78d3 100644 --- a/src/entities/RealmProvider/utils.test.ts +++ b/src/entities/RealmProvider/utils.test.ts @@ -1,15 +1,31 @@ import fetch from "node-fetch" -import { hotSceneGenesisPlaza } from "../../__data__/hotSceneGenesisPlaza" -import RealmProvider from "./utils" +import { + hotSceneGenesisPlaza, + hotSceneGenesisPlazaLegacy, +} from "../../__data__/hotSceneGenesisPlaza" +import RealmProvider, { + fetchHotScenesAndUpdateCache, + getHotScenes, +} from "./utils" jest.mock("node-fetch", () => jest.fn()) -jest.mock("decentraland-gatsby/dist/utils/env", () => - jest.fn(() => "https://realm-provider/") -) +jest.mock("decentraland-gatsby/dist/utils/env", () => { + return jest.fn((key: string) => { + switch (key) { + case "REALM_PROVIDER_URL": + return "https://realm-provider/" + case "ARCHIPELAGO_URL": + return "https://archipelago-provider/" + default: + return "https://default-provider/" + } + }) +}) describe("RealmProvider", () => { - const url = "https://realm-provider/hot-scenes" + const url = "https://archipelago-provider/hot-scenes" + const legacyUrl = "https://realm-provider/hot-scenes" const mockFetch = fetch as jest.MockedFunction beforeEach(() => { @@ -17,16 +33,27 @@ describe("RealmProvider", () => { }) it("should fetch hot scenes successfully", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: jest.fn().mockResolvedValueOnce(hotSceneGenesisPlaza), - } as any) + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([hotSceneGenesisPlazaLegacy]), + } as any) + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([hotSceneGenesisPlaza]), + } as any) const realmProvider = RealmProvider.get() - const hotScenes = await realmProvider.getHotScenes() + const [hotScenesLegacy, hotScenes] = await Promise.all([ + realmProvider.getHotScenes(true), + realmProvider.getHotScenes(), + ]) + + expect(mockFetch).toHaveBeenCalledWith(legacyUrl, expect.any(Object)) + expect(hotScenesLegacy[0]).toEqual(hotSceneGenesisPlazaLegacy) expect(mockFetch).toHaveBeenCalledWith(url, expect.any(Object)) - expect(hotScenes).toEqual(hotSceneGenesisPlaza) + expect(hotScenes[0]).toEqual(hotSceneGenesisPlaza) }) it("should handle fetch failure", async () => { @@ -41,4 +68,53 @@ describe("RealmProvider", () => { "Failed to fetch hot scenes: Internal Server Error" ) }) + + describe("fetchHotScenesAndUpdateCache", () => { + describe("when the scene is in both sources", () => { + it("should merge and cache hot scenes from both sources", async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([hotSceneGenesisPlazaLegacy]), + } as any) + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([hotSceneGenesisPlaza]), + } as any) + + await fetchHotScenesAndUpdateCache() + const result = getHotScenes() + + expect(result).toHaveLength(1) + expect(result[0].usersTotalCount).toBe(21) + }) + }) + + describe("when the scene is in only one source", () => { + it("should keep separate scenes with different coordinates", async () => { + const scene = { + ...hotSceneGenesisPlaza, + baseCoords: [100, 100], + usersTotalCount: 1, + } + + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([hotSceneGenesisPlazaLegacy]), + } as any) + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce([scene]), + } as any) + + await fetchHotScenesAndUpdateCache() + const result = getHotScenes() + + expect(result).toHaveLength(2) + expect(result[0]?.usersTotalCount).toBe(11) + expect(result[1]?.usersTotalCount).toBe(1) + }) + }) + }) }) diff --git a/src/entities/RealmProvider/utils.ts b/src/entities/RealmProvider/utils.ts index 8a9ccaca..708d39d7 100644 --- a/src/entities/RealmProvider/utils.ts +++ b/src/entities/RealmProvider/utils.ts @@ -8,40 +8,50 @@ const DEFAULT_HOST_SCENE = [] as HotScene[] let memory = DEFAULT_HOST_SCENE +// TODO: Remove urlLegacy reference once the Legacy Explorer is not longer used export default class RealmProvider { static Url = env( + "ARCHIPELAGO_URL", + "https://archipelago-ea-stats.decentraland.org/" + ) + static UrlLegacy = env( "REALM_PROVIDER_URL", "https://realm-provider.decentraland.org/" ) static Cache = new Map() private url: string + private urlLegacy: string - constructor(url: string) { + constructor(url: string, urlLegacy: string) { this.url = url + this.urlLegacy = urlLegacy } // Singleton instance based on URL - static from(url: string) { + static from(url: string, urlLegacy: string) { if (!this.Cache.has(url)) { - this.Cache.set(url, new RealmProvider(url)) + this.Cache.set(url, new RealmProvider(url, urlLegacy)) } return this.Cache.get(url)! } static get() { - return this.from(this.Url) + return this.from(this.Url, this.UrlLegacy) } - async getHotScenes(): Promise { + async getHotScenes(isLegeacy = false): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), Time.Second * 10) try { - const response = await fetch(`${this.url}hot-scenes`, { - signal: controller.signal as RequestInit["signal"], - }) + const response = await fetch( + `${isLegeacy ? this.urlLegacy : this.url}hot-scenes`, + { + signal: controller.signal as RequestInit["signal"], + } + ) if (!response.ok) { throw new Error(`Failed to fetch hot scenes: ${response.statusText}`) } @@ -52,10 +62,38 @@ export default class RealmProvider { } } +export const processScene = ( + scene: HotScene, + sceneMap: Map +) => { + const key = `${scene.baseCoords[0]},${scene.baseCoords[1]}` + if (sceneMap.has(key)) { + // If scene exists, sum the users count + const existingScene = sceneMap.get(key)! + sceneMap.set(key, { + ...existingScene, + usersTotalCount: existingScene.usersTotalCount + scene.usersTotalCount, + }) + } else { + sceneMap.set(key, scene) + } +} + export const fetchHotScenesAndUpdateCache = async () => { try { - const response = await RealmProvider.get().getHotScenes() - memory = response + const [hotScenesLegacy, hotScenes] = await Promise.all([ + RealmProvider.get().getHotScenes(true), + RealmProvider.get().getHotScenes(), + ]) + + // Create a Map to store scenes by their baseCoords + const scenesMap = new Map() + + // Process both arrays + hotScenesLegacy.forEach((scene) => processScene(scene, scenesMap)) + hotScenes.forEach((scene) => processScene(scene, scenesMap)) + + memory = Array.from(scenesMap.values()) } catch (error) { memory = DEFAULT_HOST_SCENE }