From 38c5247deddf075d3fe6f8b52ab1749a47bce306 Mon Sep 17 00:00:00 2001 From: Andrea Vos Date: Sat, 3 Jul 2021 01:15:44 +0200 Subject: [PATCH] fix cache invalidation --- server/routes/admin.js | 6 +++--- server/routes/banner.js | 5 ++--- server/routes/blog.js | 4 ++-- server/routes/inclusive.js | 24 +++++++++++++++-------- server/routes/nouns.js | 24 +++++++++++++++-------- server/routes/profile.js | 6 ++++++ server/routes/terms.js | 9 +++++++-- src/cache.js | 40 +++++++++++++++++++++++++++----------- 8 files changed, 81 insertions(+), 37 deletions(-) diff --git a/server/routes/admin.js b/server/routes/admin.js index e30207917..3f499b838 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -6,12 +6,12 @@ import {buildDict, now, shuffle, handleErrorAsync, buildLocaleList} from "../../ import locales from '../../src/locales'; import {calculateStats, statsFile} from '../../src/stats'; import fs from 'fs'; -import cache from "../../src/cache"; +import { caches } from "../../src/cache"; const router = Router(); router.get('/admin/list', handleErrorAsync(async (req, res) => { - return res.json(await cache('main', 'admins.js', 10, async () => { + return res.json(await caches.admins.fetch(async () => { const admins = await req.db.all(SQL` SELECT u.username, p.teamName, p.locale, u.id, u.email, u.avatarSource FROM users u @@ -47,7 +47,7 @@ router.get('/admin/list', handleErrorAsync(async (req, res) => { })); router.get('/admin/list/footer', handleErrorAsync(async (req, res) => { - return res.json(shuffle(await cache('main', 'footer.js', 10, async () => { + return res.json(shuffle(await caches.adminsFooter.fetch(async () => { const fromDb = await req.db.all(SQL` SELECT u.username, p.footerName, p.footerAreas, p.locale FROM users u diff --git a/server/routes/banner.js b/server/routes/banner.js index 64103118f..8c6158aa9 100644 --- a/server/routes/banner.js +++ b/server/routes/banner.js @@ -6,8 +6,7 @@ import avatar from '../avatar'; import {buildPronoun, parsePronouns} from "../../src/buildPronoun"; import {loadTsv} from "../../src/tsv"; import {handleErrorAsync} from "../../src/helpers"; -import cache from "../../src/cache"; -import fs from "fs"; +import { CacheObject } from "../../src/cache"; const translations = loadSuml('translations'); @@ -38,7 +37,7 @@ router.get('/banner/:pronounName*.png', handleErrorAsync(async (req, res) => { const pronounName = req.params.pronounName + req.params[0]; - const result = await cache('banner', `${pronounName}.png`, 24 * 60, async () => { + const result = await new CacheObject('banner', `${pronounName}.png`, 24 * 60).fetch(async () => { registerFont('static/fonts/quicksand-v21-latin-ext_latin-regular.ttf', {family: 'Quicksand', weight: 'regular'}); registerFont('static/fonts/quicksand-v21-latin-ext_latin-700.ttf', {family: 'Quicksand', weight: 'bold'}); diff --git a/server/routes/blog.js b/server/routes/blog.js index e1593f93b..ba1f4f909 100644 --- a/server/routes/blog.js +++ b/server/routes/blog.js @@ -1,12 +1,12 @@ import { Router } from 'express'; import {handleErrorAsync, shuffle} from "../../src/helpers"; import fs from 'fs'; -import cache from "../../src/cache"; +import { caches } from "../../src/cache"; const router = Router(); router.get('/blog', handleErrorAsync(async (req, res) => { - return res.json(await cache('main', 'blog.js', Infinity, async () => { + return res.json(await caches.blog.fetch(async () => { const dir = __dirname + '/../../data/blog'; const posts = []; fs.readdirSync(dir).forEach(file => { diff --git a/server/routes/inclusive.js b/server/routes/inclusive.js index 17cfad57e..5fef4e119 100644 --- a/server/routes/inclusive.js +++ b/server/routes/inclusive.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import SQL from 'sql-template-strings'; import {ulid} from "ulid"; import {isTroll, handleErrorAsync} from "../../src/helpers"; +import { caches } from "../../src/cache"; const approve = async (db, id) => { const { base_id } = await db.get(SQL`SELECT base_id FROM inclusive WHERE id=${id}`); @@ -17,19 +18,22 @@ const approve = async (db, id) => { SET approved = 1, base_id = NULL WHERE id = ${id} `); + await caches.inclusive.invalidate(); } const router = Router(); router.get('/inclusive', handleErrorAsync(async (req, res) => { - return res.json(await req.db.all(SQL` - SELECT i.*, u.username AS author FROM inclusive i - LEFT JOIN users u ON i.author_id = u.id - WHERE i.locale = ${global.config.locale} - AND i.approved >= ${req.isGranted('inclusive') ? 0 : 1} - AND i.deleted = 0 - ORDER BY i.approved, i.insteadOf - `)); + return res.json(await caches.inclusive.fetch(async () => { + return await req.db.all(SQL` + SELECT i.*, u.username AS author FROM inclusive i + LEFT JOIN users u ON i.author_id = u.id + WHERE i.locale = ${global.config.locale} + AND i.approved >= ${req.isGranted('inclusive') ? 0 : 1} + AND i.deleted = 0 + ORDER BY i.approved, i.insteadOf + `); + })); })); router.get('/inclusive/search/:term', handleErrorAsync(async (req, res) => { @@ -79,6 +83,8 @@ router.post('/inclusive/hide/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.inclusive.invalidate(); + return res.json('ok'); })); @@ -103,6 +109,8 @@ router.post('/inclusive/remove/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.inclusive.invalidate(); + return res.json('ok'); })); diff --git a/server/routes/nouns.js b/server/routes/nouns.js index b02ada00f..6fcd29227 100644 --- a/server/routes/nouns.js +++ b/server/routes/nouns.js @@ -4,6 +4,7 @@ import {ulid} from "ulid"; import {createCanvas, loadImage, registerFont} from "canvas"; import {loadSuml} from "../loader"; import {handleErrorAsync, isTroll} from "../../src/helpers"; +import { caches } from "../../src/cache"; const translations = loadSuml('translations'); @@ -21,6 +22,7 @@ const approve = async (db, id) => { SET approved = 1, base_id = NULL WHERE id = ${id} `); + await caches.nouns.invalidate(); } const addVersions = async (req, nouns) => { @@ -69,14 +71,16 @@ const selectFragment = (sourcesMap, keyAndFragment) => { const router = Router(); router.get('/nouns', handleErrorAsync(async (req, res) => { - return res.json(await addVersions(req, await req.db.all(SQL` - SELECT n.*, u.username AS author FROM nouns n - LEFT JOIN users u ON n.author_id = u.id - WHERE n.locale = ${global.config.locale} - AND n.deleted = 0 - AND n.approved >= ${req.isGranted('nouns') ? 0 : 1} - ORDER BY n.approved, n.masc - `))); + return res.json(await caches.nouns.fetch(async () => { + return await addVersions(req, await req.db.all(SQL` + SELECT n.*, u.username AS author FROM nouns n + LEFT JOIN users u ON n.author_id = u.id + WHERE n.locale = ${global.config.locale} + AND n.deleted = 0 + AND n.approved >= ${req.isGranted('nouns') ? 0 : 1} + ORDER BY n.approved, n.masc + `)) + })); })); router.get('/nouns/search/:term', handleErrorAsync(async (req, res) => { @@ -127,6 +131,8 @@ router.post('/nouns/hide/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.nouns.invalidate(); + return res.json('ok'); })); @@ -151,6 +157,8 @@ router.post('/nouns/remove/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.nouns.invalidate(); + return res.json('ok'); })); diff --git a/server/routes/profile.js b/server/routes/profile.js index 61d3ae295..207c4ab96 100644 --- a/server/routes/profile.js +++ b/server/routes/profile.js @@ -4,6 +4,7 @@ import md5 from "js-md5"; import {ulid} from "ulid"; import avatar from "../avatar"; import {handleErrorAsync} from "../../src/helpers"; +import { caches } from "../../src/cache"; const normalise = s => s.trim().toLowerCase(); @@ -102,6 +103,11 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => { )`); } + if (req.body.teamName) { + await caches.admins.invalidate(); + await caches.adminsFooter.invalidate(); + } + return res.json(await fetchProfiles(req.db, req.user.username, true)); })); diff --git a/server/routes/terms.js b/server/routes/terms.js index 5a50ddec0..b49109f93 100644 --- a/server/routes/terms.js +++ b/server/routes/terms.js @@ -2,7 +2,7 @@ import { Router } from 'express'; import SQL from 'sql-template-strings'; import {ulid} from "ulid"; import {isTroll, handleErrorAsync} from "../../src/helpers"; -import cache from "../../src/cache"; +import { caches } from "../../src/cache"; const approve = async (db, id) => { const { base_id } = await db.get(SQL`SELECT base_id FROM terms WHERE id=${id}`); @@ -18,12 +18,13 @@ const approve = async (db, id) => { SET approved = 1, base_id = NULL WHERE id = ${id} `); + await caches.terms.invalidate(); } const router = Router(); router.get('/terms', handleErrorAsync(async (req, res) => { - return res.json(await cache('main', 'terms.js', 10, () => { + return res.json(await caches.terms.fetch(() => { return req.db.all(SQL` SELECT i.*, u.username AS author FROM terms i LEFT JOIN users u ON i.author_id = u.id @@ -82,6 +83,8 @@ router.post('/terms/hide/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.terms.invalidate(); + return res.json('ok'); })); @@ -106,6 +109,8 @@ router.post('/terms/remove/:id', handleErrorAsync(async (req, res) => { WHERE id = ${req.params.id} `); + await caches.terms.invalidate(); + return res.json('ok'); })); diff --git a/src/cache.js b/src/cache.js index 20ad09a33..6f2043245 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,18 +1,36 @@ import fs from 'fs'; -export default async (dir, filename, maxAgeMinutes, generator) => { - const cacheDir = `${__dirname}/../cache/${dir}` - fs.mkdirSync(cacheDir, { recursive: true }); - const cacheFilename = `${cacheDir}/${filename}`; - - if (fs.existsSync(cacheFilename) && fs.statSync(cacheFilename).mtimeMs >= (new Date() - maxAgeMinutes*60*1000)) { - const content = fs.readFileSync(cacheFilename); - return filename.endsWith('.js') ? JSON.parse(content) : content; +export class CacheObject { + constructor(dir, filename, maxAgeMinutes) { + const cacheDir = `${__dirname}/../cache/${dir}` + fs.mkdirSync(cacheDir, { recursive: true }); + this.path = `${cacheDir}/${filename}`; + this.maxAgeMinutes = maxAgeMinutes; } - const result = await generator(); + async fetch(generator) { + if (fs.existsSync(this.path) && fs.statSync(this.path).mtimeMs >= (new Date() - this.maxAgeMinutes*60*1000)) { + const content = fs.readFileSync(this.path); + return this.path.endsWith('.js') ? JSON.parse(content) : content; + } - fs.writeFileSync(cacheFilename, filename.endsWith('.js') ? JSON.stringify(result) : result); + const result = await generator(); - return result; + fs.writeFileSync(this.path, this.path.endsWith('.js') ? JSON.stringify(result) : result); + + return result; + } + + async invalidate() { + fs.unlinkSync(this.path); + } +} + +export const caches = { + admins: new CacheObject('main', 'admins.js', 10), + adminsFooter: new CacheObject('main', 'footer.js', 10), + blog: new CacheObject('main', 'blog.js', Infinity), + nouns: new CacheObject('main', 'nouns.js', 10), + terms: new CacheObject('main', 'terms.js', 10), + inclusive: new CacheObject('main', 'inclusive.js', 10), }