Skip to content

Commit cbc9d38

Browse files
LinaBellclaude
andcommitted
Add inventory cache with 6h TTL and forceRefreshInventory support
Cache ecosystem inventory items in memory to reduce API calls. Both getInventoryItems and awardBadge now use the shared cache. The client reads forceRefreshInventory from URL search params and passes it through to the server, which bypasses the cache when set to true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6167a4c commit cbc9d38

File tree

6 files changed

+63
-27
lines changed

6 files changed

+63
-27
lines changed

client/src/pages/Home.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useContext, useState, useEffect } from "react";
2+
import { useSearchParams } from "react-router-dom";
23

34
// components
45
import {
@@ -22,13 +23,17 @@ import { backendAPI, loadGameState } from "@utils";
2223
function Home() {
2324
const dispatch = useContext(GlobalDispatchContext);
2425
const { screenManager } = useContext(GlobalStateContext);
26+
27+
const [searchParams] = useSearchParams();
28+
const forceRefreshInventory = searchParams.get("forceRefreshInventory") === "true";
29+
2530
const [loading, setLoading] = useState(true);
2631

2732
useEffect(() => {
2833
const fetchGameState = async () => {
2934
try {
3035
setLoading(true);
31-
await loadGameState(dispatch);
36+
await loadGameState(dispatch, forceRefreshInventory);
3237
} catch (error) {
3338
console.error("error in loadGameState action");
3439
} finally {

client/src/utils/loadGameState.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { backendAPI, getErrorMessage } from "@utils";
22
import { LOAD_GAME_STATE, SCREEN_MANAGER, SET_ERROR } from "@context/types";
33

4-
export const loadGameState = async (dispatch) => {
4+
export const loadGameState = async (dispatch, forceRefreshInventory) => {
55
try {
6-
const result = await backendAPI?.post("/race/game-state");
6+
const result = await backendAPI?.post("/race/game-state", { forceRefreshInventory });
77
if (result?.data?.success) {
88
const {
99
checkpointsCompleted,

server/controllers/handleLoadGameState.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const handleLoadGameState = async (req, res) => {
1212
try {
1313
const credentials = getCredentials(req.query);
1414
const { profileId, urlSlug, sceneDropId } = credentials;
15+
const forceRefresh = req.body?.forceRefreshInventory === true;
1516
const now = Date.now();
1617

1718
const world = await World.create(urlSlug, { credentials });
@@ -70,7 +71,7 @@ export const handleLoadGameState = async (req, res) => {
7071
const { visitor, visitorProgress, visitorInventory } = await getVisitor(credentials, true);
7172
let { checkpoints, startTimestamp } = visitorProgress;
7273

73-
const { badges } = await getInventoryItems(credentials);
74+
const { badges } = await getInventoryItems(credentials, { forceRefresh });
7475

7576
return res.json({
7677
checkpointsCompleted: checkpoints,

server/utils/badges/awardBadge.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Ecosystem } from "../topiaInit.js";
1+
import { getCachedInventoryItems } from "../inventoryCache.js";
22

33
export const awardBadge = async ({ credentials, visitor, visitorInventory, badgeName, redisObj, profileId }) => {
44
try {
55
if (visitorInventory[badgeName]) return { success: true };
66

7-
const ecosystem = await Ecosystem.create({ credentials });
8-
await ecosystem.fetchInventoryItems();
9-
10-
const inventoryItem = ecosystem.inventoryItems?.find((item) => item.name === badgeName);
7+
const inventoryItems = await getCachedInventoryItems({ credentials });
8+
const inventoryItem = inventoryItems?.find((item) => item.name === badgeName);
119
if (!inventoryItem) throw new Error(`Inventory item ${badgeName} not found in ecosystem`);
1210

1311
await visitor.grantInventoryItem(inventoryItem, 1);

server/utils/badges/getInventoryItems.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Ecosystem } from "../index.js";
1+
import { getCachedInventoryItems } from "../inventoryCache.js";
22

3-
export const getInventoryItems = async (credentials) => {
3+
export const getInventoryItems = async (credentials, { forceRefresh = false } = {}) => {
44
try {
5-
const ecosystem = await Ecosystem.create({ credentials });
6-
await ecosystem.fetchInventoryItems();
5+
const items = await getCachedInventoryItems({ credentials, forceRefresh });
76

87
const badges = {};
9-
10-
for (const item of ecosystem.inventoryItems) {
8+
for (const item of items) {
119
badges[item.name] = {
1210
id: item.id,
1311
name: item.name || "Unknown",
@@ -16,18 +14,7 @@ export const getInventoryItems = async (credentials) => {
1614
};
1715
}
1816

19-
// Sort items by sortOrder while keeping them as objects
20-
const sortedBadges = {};
21-
22-
Object.values(badges)
23-
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
24-
.forEach((badge) => {
25-
sortedBadges[badge.name] = badge;
26-
});
27-
28-
return {
29-
badges: sortedBadges,
30-
};
17+
return { badges };
3118
} catch (error) {
3219
return standardizeError(error);
3320
}

server/utils/inventoryCache.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Ecosystem } from "./topiaInit.js";
2+
3+
// Cache duration: 6 hours in milliseconds
4+
const CACHE_DURATION_MS = 6 * 60 * 60 * 1000;
5+
6+
// In-memory cache
7+
let inventoryCache = null;
8+
9+
/**
10+
* Get ecosystem inventory items with caching.
11+
* - Returns cached data if available and not expired.
12+
* - Refreshes cache if expired or missing.
13+
* - Falls back to stale cache on API failure.
14+
*/
15+
export const getCachedInventoryItems = async ({ credentials, forceRefresh = false }) => {
16+
try {
17+
const now = Date.now();
18+
const isCacheValid = inventoryCache !== null && !forceRefresh && now - inventoryCache.timestamp < CACHE_DURATION_MS;
19+
20+
if (isCacheValid) {
21+
return inventoryCache.items;
22+
}
23+
24+
console.log("Fetching fresh inventory items from ecosystem");
25+
const ecosystem = await Ecosystem.create({ credentials });
26+
await ecosystem.fetchInventoryItems();
27+
28+
inventoryCache = {
29+
items: [...ecosystem.inventoryItems].sort((a, b) => (a.metadata?.sortOrder ?? 0) - (b.metadata?.sortOrder ?? 0)),
30+
timestamp: now,
31+
};
32+
33+
return inventoryCache.items;
34+
} catch (error) {
35+
if (inventoryCache !== null) {
36+
console.warn("Failed to fetch fresh inventory, using stale cache", error);
37+
return inventoryCache.items;
38+
}
39+
throw error;
40+
}
41+
};
42+
43+
export const clearInventoryCache = () => {
44+
inventoryCache = null;
45+
};

0 commit comments

Comments
 (0)