Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Most active Places Sort by #570

Merged
merged 7 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 392 additions & 2 deletions src/__data__/hotSceneGenesisPlaza.ts

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions src/entities/Place/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,7 @@ export default class PlaceModel extends Model<PlaceAttributes> {
)

const filterMostActivePlaces =
options.order_by === PlaceListOrderBy.MOST_ACTIVE &&
!!options.hotScenesPositions &&
options.hotScenesPositions.length > 0
!!options.hotScenesPositions && options.hotScenesPositions.length > 0

const sql = SQL`
${conditional(
Expand Down
7 changes: 4 additions & 3 deletions src/entities/Place/routes/getPlace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ 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"

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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/entities/Place/routes/getPlace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions src/entities/Place/routes/getPlaceList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/entities/Place/routes/getPlaceList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions src/entities/Place/routes/getPlaceMostActiveList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/entities/Place/routes/getPlaceUserVisitsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
100 changes: 88 additions & 12 deletions src/entities/RealmProvider/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,59 @@
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<typeof fetch>

beforeEach(() => {
jest.clearAllMocks()
})

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 () => {
Expand All @@ -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)
})
})
})
})
58 changes: 48 additions & 10 deletions src/entities/RealmProvider/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RealmProvider>()

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<HotScene[]> {
async getHotScenes(isLegeacy = false): Promise<HotScene[]> {
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}`)
}
Expand All @@ -52,10 +62,38 @@ export default class RealmProvider {
}
}

export const processScene = (
scene: HotScene,
sceneMap: Map<string, HotScene>
) => {
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<string, HotScene>()

// 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
}
Expand Down