Skip to content

Commit 5de68c3

Browse files
committed
add redis cache
1 parent d6a0911 commit 5de68c3

7 files changed

Lines changed: 324 additions & 105 deletions

File tree

index.js

Lines changed: 190 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ const PORT = process.env.PORT || 3000
1313

1414
const NOTION_FUNDRAISER_DATABASE_ID = "11ee959b7fdb4204a9ce46c9224b1818";
1515
const NOTION_STORE_DATABASE_ID = "7c36ef34cebb48e097706442634abaaf";
16+
const redis = require('redis');
17+
const client = redis.createClient(); // Connects to default 127.0.0.1:6379 on your Droplet
1618

19+
client.on('error', (err) => console.log('Redis Client Error', err));
20+
client.connect(); // v4 of redis requires explicit connect
21+
const CACHE_TTL = 3500; // 60 minutes in seconds
1722
console.log("starting up")
1823
app.use(express.static("public"))
1924
app.use( bodyParser.json() );
@@ -178,16 +183,21 @@ app.get("/sex-ed/:slug", async (req,res) => {
178183
// res.render("networked-performance/session")
179184
// })
180185

181-
app.get("/classes", async (req,res) => {
182-
const response = await getDatabaseEntries("6d5585af2f544dd1bad9d24c5e177026", [{property:"Date", direction:"descending"}])
183-
const projectData = response.map((project) => {
184-
console.log(project)
185-
return parseClassData(project)
186-
})
187-
console.log(projectData)
188-
// let pageContent = getPageContent()
189-
res.render("programs/classes", {projects: projectData})
190-
})
186+
app.get("/classes", async (req, res) => {
187+
try {
188+
const classes = await getCachedData("notion:classes:list", async () => {
189+
const response = await getDatabaseEntries("6d5585af2f544dd1bad9d24c5e177026", [
190+
{ property: "Date", direction: "descending" }
191+
]);
192+
return response.map(project => parseClassData(project));
193+
});
194+
195+
res.render("programs/classes", { projects:classes });
196+
} catch (error) {
197+
console.error("Classes Error:", error);
198+
res.status(500).send("Error loading classes");
199+
}
200+
});
191201

192202

193203
app.get("/sessions", async (req,res) => {
@@ -1002,36 +1012,72 @@ app.get("/sessions/:slug", async (req, res) => {
10021012

10031013

10041014
app.get("/sessions/:session/:class", async(req,res) => {
1005-
const data = await getDatabaseEntry("6d5585af2f544dd1bad9d24c5e177026", {"and":[{property:"Website-Slug", "rich_text": {"equals":req.params.class}}, {property:"Session Slug", "rollup": { "any": { "rich_text": { "equals": req.params.session } }}}]})
1006-
// const data = await getDatabaseEntry("57406c3b209e4bfba3953de6328086ac", {"and":[{property:"Website-Slug", "rich_text": {"equals":req.params.class}}, {property:"Session Slug", "rollup": { "any": { "rich_text": { "equals": req.params.session } }}}]})
1007-
if(!data) return
1008-
const response = await prepareClassData(data, req.params.class)
1009-
res.render("programs/class-concurrent", response);
1015+
const { session, class: className } = req.params;
1016+
const cacheKey = `cache:session:${session}:class:${className}`;
10101017

1011-
})
1018+
try {
1019+
// 1. Try to get data from Redis
1020+
const cachedData = await client.get(cacheKey);
10121021

1022+
if (cachedData) {
1023+
// 2. Serve from cache immediately
1024+
const parsedCache = JSON.parse(cachedData);
1025+
res.render("programs/class-concurrent", parsedCache);
1026+
1027+
// 3. Background Revalidation: Update cache without making user wait
1028+
revalidateClassData(session, className, cacheKey).catch(console.error);
1029+
} else {
1030+
// 4. Cache Miss: Fetch from Notion (Only happens once per TTL)
1031+
const freshData = await revalidateClassData(session, className, cacheKey);
1032+
if (freshData) {
1033+
res.render("programs/class-concurrent", freshData);
1034+
} else {
1035+
res.status(404).send("Class not found");
1036+
}
1037+
}
1038+
} catch (error) {
1039+
console.error("Route Error:", error);
1040+
res.status(500).send("Internal Server Error");
1041+
}
10131042

1014-
app.get("/projects", async (req,res) => {
1015-
const response = await getDatabaseEntries("713f24806a524c5e892971e4fbf5c9dd", [{property:"Release Date", direction:"descending"}])
1016-
const projectData = response.map((project) => {
1017-
console.log(project)
1018-
return parseNotionPage(project)
1019-
})
1020-
console.log(projectData)
1021-
res.render("projects/projectList", {projects: projectData})
10221043
})
10231044

1024-
app.get("/projects/:slug", async (req,res) => {
1025-
//filter by slug here
1026-
console.log(req.params.slug)
1027-
const response = await getDatabaseEntry("713f24806a524c5e892971e4fbf5c9dd", {property:"Website-Slug", "rich_text": {"equals":req.params.slug}})
1028-
console.log(response)
1029-
if(response){
1030-
const projectData = parseProjectData(response)
1031-
console.log(projectData)
1032-
res.render("projects/projectPage", projectData)
1045+
1046+
app.get("/projects", async (req, res) => {
1047+
try {
1048+
const projects = await getCachedData("notion:projects:list", async () => {
1049+
const response = await getDatabaseEntries("713f24806a524c5e892971e4fbf5c9dd", [
1050+
{ property: "Release Date", direction: "descending" }
1051+
]);
1052+
return response.map(project => parseNotionPage(project));
1053+
});
1054+
1055+
res.render("projects/projectList", { projects });
1056+
} catch (error) {
1057+
res.status(500).send("Error");
10331058
}
1034-
})
1059+
});
1060+
1061+
app.get("/projects/:slug", async (req, res) => {
1062+
const { slug } = req.params;
1063+
try {
1064+
const projectData = await getCachedData(`notion:project:${slug}`, async () => {
1065+
const response = await getDatabaseEntry("713f24806a524c5e892971e4fbf5c9dd", {
1066+
property: "Website-Slug",
1067+
rich_text: { equals: slug }
1068+
});
1069+
return response ? parseProjectData(response) : null;
1070+
});
1071+
1072+
if (projectData) {
1073+
res.render("projects/projectPage", projectData);
1074+
} else {
1075+
res.status(404).send("Project not found");
1076+
}
1077+
} catch (error) {
1078+
res.status(500).send("Error");
1079+
}
1080+
});
10351081

10361082
app.get("/yearbook", async (req,res) => {
10371083
// const response = await getDatabaseEntries("ea99608272e446cd880cbcb8d2ee1e13", [{timestamp:"created_time", direction:"descending"}], {
@@ -1070,38 +1116,62 @@ app.get("/yearbook/:session", async (req,res) => {
10701116
})
10711117

10721118

1073-
app.get("/blog", async (req,res) => {
1074-
const response = await getDatabaseEntries("5fb49fe53804424a89230294206fcaee", [{property:"Publish-Date", direction:"descending"}])
1075-
const blogData = response.map((blog) => {
1076-
console.log(blog)
1077-
return parseBlog(blog)
1078-
})
1079-
console.log(blogData)
1080-
res.render("blog/blog", {blog: blogData})
1119+
app.get("/blog", async (req, res) => {
1120+
try {
1121+
const blog = await getCachedData("cache:blog:list", async () => {
1122+
const response = await getDatabaseEntries("5fb49fe53804424a89230294206fcaee", [
1123+
{ property: "Publish-Date", direction: "descending" }
1124+
]);
1125+
return response.map(post => parseBlog(post));
1126+
});
10811127

1082-
})
1128+
res.render("blog/blog", { blog });
1129+
} catch (error) {
1130+
console.error(error);
1131+
res.status(500).send("Error loading blog");
1132+
}
1133+
});
10831134

10841135

10851136

10861137

10871138

1139+
app.get("/blog/:slug", async (req, res) => {
1140+
const { slug } = req.params;
1141+
const cacheKey = `cache:blog:${slug}`;
10881142

1143+
try {
1144+
const cachedData = await client.get(cacheKey);
10891145

1090-
app.get("/blog/:slug", async (req,res) => {
1091-
const response = await getDatabaseEntry("5fb49fe53804424a89230294206fcaee", {property:"Website-Slug", "rich_text": {"equals":req.params.slug}})
1092-
const parsedData = parseNotionPage(response)
1146+
if (cachedData) {
1147+
// 1. Serve immediately from Redis
1148+
const parsedCache = JSON.parse(cachedData);
1149+
res.render("blog/post", {
1150+
title: parsedCache.Name,
1151+
postHTML: parsedCache.postHTML,
1152+
...parsedCache
1153+
});
10931154

1094-
// const blogData = response.map((blog) => {
1095-
// console.log(blog)
1096-
// return parseNotionPage(blog)
1097-
// })
1098-
1099-
console.log(parsedData)
1100-
const pageContent = await getBlocks(response.id)
1101-
let postHTML = parsePageContentHTML(pageContent)
1102-
console.log(postHTML)
1103-
res.render("blog/post", {title: parsedData.Name, postHTML:postHTML, ...parsedData})
1104-
})
1155+
// 2. Update the cache in the background for the next person
1156+
revalidateBlogData(slug, cacheKey).catch(console.error);
1157+
} else {
1158+
// 3. Cache Miss: Fetch and wait
1159+
const freshData = await revalidateBlogData(slug, cacheKey);
1160+
if (freshData) {
1161+
res.render("blog/post", {
1162+
title: freshData.Name,
1163+
postHTML: freshData.postHTML,
1164+
...freshData
1165+
});
1166+
} else {
1167+
res.status(404).send("Post not found");
1168+
}
1169+
}
1170+
} catch (error) {
1171+
console.error("Blog Route Error:", error);
1172+
res.status(500).send("Internal Server Error");
1173+
}
1174+
});
11051175

11061176

11071177
// app.get("/ecpc", async (req,res) => {
@@ -2349,3 +2419,66 @@ function formatRichText(textArray) {
23492419
}
23502420
return formattedText
23512421
}
2422+
2423+
async function getCachedData(key, fetcher) {
2424+
const cached = await client.get(key);
2425+
2426+
if (cached) {
2427+
// Optional: Trigger background update if stale (Advanced ISR)
2428+
return JSON.parse(cached);
2429+
}
2430+
2431+
// Cache Miss: Fetch fresh data
2432+
const freshData = await fetcher();
2433+
2434+
// Store in Redis
2435+
await client.set(key, JSON.stringify(freshData), {
2436+
EX: CACHE_TTL
2437+
});
2438+
2439+
return freshData;
2440+
}
2441+
async function revalidateClassData(sessionSlug, classSlug, cacheKey) {
2442+
// Your original query logic from index.js
2443+
const data = await getDatabaseEntry("6d5585af2f544dd1bad9d24c5e177026", {
2444+
"and": [
2445+
{ property: "Website-Slug", "rich_text": { "equals": classSlug } },
2446+
{ property: "Session Slug", "rollup": { "any": { "rich_text": { "equals": sessionSlug } } } }
2447+
]
2448+
});
2449+
2450+
if (!data) return null;
2451+
2452+
// Your original processing logic
2453+
const response = await prepareClassData(data, classSlug);
2454+
2455+
// Store in Redis (Expire after 1 hour to account for image URL expiration)
2456+
await client.set(cacheKey, JSON.stringify(response), {
2457+
EX: 3500
2458+
});
2459+
2460+
return response;
2461+
}
2462+
async function revalidateBlogData(slug, cacheKey) {
2463+
const response = await getDatabaseEntry("5fb49fe53804424a89230294206fcaee", {
2464+
property: "Website-Slug",
2465+
rich_text: { "equals": slug }
2466+
});
2467+
2468+
if (!response) return null;
2469+
2470+
const parsedData = parseNotionPage(response);
2471+
const pageContent = await getBlocks(response.id);
2472+
2473+
// Note: If you implement image downloading, make sure this is awaited
2474+
const postHTML = parsePageContentHTML(pageContent);
2475+
2476+
const finalData = { ...parsedData, postHTML };
2477+
2478+
// Update Redis with a TTL (e.g., 1 hour)
2479+
await client.set(cacheKey, JSON.stringify(finalData), {
2480+
EX: 3500
2481+
});
2482+
2483+
return finalData;
2484+
}

0 commit comments

Comments
 (0)