Skip to content

Commit

Permalink
#3546 - Added event api endpoint caching (#3549)
Browse files Browse the repository at this point in the history
* Added event api endpoint caching

Using npm stale-while-revalidate-cache package

* Added cache hardening

- Using `lru-cache` to limit memory growth
- Limiting cache key combinations to avoid key polluting
- Max length of presenter to avoid large key sizes
- LRU cache expires after 1 hour
  • Loading branch information
christianmorfordwaitessw authored Jan 28, 2025
1 parent 9a75e9e commit 05a5aab
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 11 deletions.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@next/eslint-plugin-next": "14.2.15",
"@playwright/test": "^1.48.2",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.4.6",
Expand All @@ -33,22 +34,21 @@
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.2",
"@next/eslint-plugin-next": "14.2.15",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.15",
"eslint-plugin-tailwindcss": "^3.17.5",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-tailwindcss": "^3.17.5",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"next-sitemap": "^4.2.3",
"playwright": "^1.48.2",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"source-map-explorer": "^2.5.3",
"ts-node": "^10.9.2"
},
Expand Down Expand Up @@ -82,6 +82,7 @@
"formik": "^2.4.6",
"framer-motion": "^11.12.0",
"lodash": "^4.17.21",
"lru-cache": "^11.0.2",
"lucide-react": "^0.460.0",
"next": "^14.2.15",
"next-plugin-preval": "^1.2.6",
Expand All @@ -101,6 +102,7 @@
"schema-dts": "^1.1.2",
"sharp": "^0.33.5",
"ssw.megamenu": "4.9.4",
"stale-while-revalidate-cache": "^3.4.0",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.14",
"tailwindcss-gradients": "^3.0.0",
Expand Down
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 74 additions & 7 deletions services/server/getEvents.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
import client from "@/tina/client";
import { LRUCache } from "lru-cache";
import { createStaleWhileRevalidateCache } from "stale-while-revalidate-cache";

const WEBSITE_URL = "https://www.ssw.com.au";
export const EVENTS_MAX_SIZE_OVERRIDE = 999;
const DEFAULT_PAGE_SIZE = 10;

export const getPastEvents = async (top, presenterName) => {
const CACHE_MAX_TTL = 60 * 60 * 1000; // 60 mins
const CACHE_STALE_TIME = 15 * 60 * 1000; // 15 mins
const CACHE_LRU_MAX_ENTRIES = 1000; // Maximum number of cache entries

const VALID_TOP_VALUES = ["10", "20", "50"]; // Limit the values that are cached
const MAX_PRESENTER_NAME_LENGTH = 100; // Limit the cache key length

const inMemoryStorage = {
store: new LRUCache({
max: CACHE_LRU_MAX_ENTRIES,
ttl: CACHE_MAX_TTL,
}),

getItem: async (key) => inMemoryStorage.store.get(key) || null,
setItem: async (key, value) => {
inMemoryStorage.store.set(key, value);
},
removeItem: async (key) => {
inMemoryStorage.store.delete(key);
},
};

const swr = createStaleWhileRevalidateCache({
storage: inMemoryStorage,
});

const configOverrides = {
maxTimeToLive: CACHE_MAX_TTL,
minTimeToStale: CACHE_STALE_TIME,
};

const isValidTop = (top) =>
top === undefined || top === null || VALID_TOP_VALUES.includes(top);

const normalizePresenterName = (name) => {
const trimmedName = name?.trim().toLowerCase().replaceAll(" ", "-") || "";
return trimmedName.slice(0, MAX_PRESENTER_NAME_LENGTH);
};

const generateCacheKey = (type, top, presenterName) => {
if (!isValidTop(top)) return null;

const normalizedTop = top === undefined ? DEFAULT_PAGE_SIZE : top;
const normalizedPresenterName = normalizePresenterName(presenterName);
return `${type}-${normalizedTop}-${normalizedPresenterName}`;
};

const getEvents = async (type: "past" | "upcoming", top, presenterName) => {
const fetchEvents = type === "past" ? fetchPastEvents : fetchUpcomingEvents;

const cacheKey = generateCacheKey(type, top, presenterName);
if (!cacheKey) {
return fetchEvents(top, presenterName);
}

return (
await swr(cacheKey, () => fetchEvents(top, presenterName), configOverrides)
).value;
};

const fetchPastEvents = async (top, presenterName) => {
const eventClient = await client.queries.getPastEventsQuery(
formatEventParams(top, presenterName)
);
return await fetchEventsWithClient(eventClient, presenterName, top);
};

export const getUpcomingEvents = async (top, presenterName) => {
const fetchUpcomingEvents = async (top, presenterName) => {
const eventClient = await client.queries.getFutureEventsQuery(
formatEventParams(top, presenterName)
);
Expand Down Expand Up @@ -48,19 +110,18 @@ const formatEvent = (event) => {
HasVideo: event.youTubeId ? "Yes" : "No",
YouTubeId: event.youTubeId,
PresenterDescription:
event.presenterList && event.presenterListlength > 0
event.presenterList && event.presenterList.length > 0
? event.presenterList[0].presenter.about
: null,
};
};

export const fetchEventsWithClient = async (
eventClient,
presenterName: string | undefined,
top
) => {
if (top) {
top = parseInt(top);
}
const normalizedTop = parseInt(top) || DEFAULT_PAGE_SIZE;
const events = [];
/* TODO: remove back end filtering after fixing events with multiple presenters in the name
https://github.com/SSWConsulting/SSW.Website/issues/2833 */
Expand All @@ -70,7 +131,7 @@ export const fetchEventsWithClient = async (
events.push(formatEvent(event.node));
}

if (events.length === (top || DEFAULT_PAGE_SIZE)) {
if (events.length === normalizedTop) {
break;
}
}
Expand Down Expand Up @@ -120,3 +181,9 @@ type eventEdge = {
presenterName: string | null | undefined;
};
};

export const getPastEvents = (top, presenterName) =>
getEvents("past", top, presenterName);

export const getUpcomingEvents = (top, presenterName) =>
getEvents("upcoming", top, presenterName);

0 comments on commit 05a5aab

Please sign in to comment.