feat: unified cache handler

This commit is contained in:
Huskydog9988
2025-05-07 22:13:22 -04:00
parent 5aa0899bcf
commit 731499be81
7 changed files with 86 additions and 34 deletions

View File

@ -48,6 +48,20 @@ export default defineNuxtConfig({
}, },
compressPublicAssets: true, compressPublicAssets: true,
storage: {
appCache: {
driver: "lru-cache",
},
},
devStorage: {
appCache: {
// store cache on fs to handle dev server restarts
driver: "fs",
base: "./.data/appCache",
},
},
}, },
typescript: { typescript: {

View File

@ -31,7 +31,6 @@
"fast-fuzzy": "^1.12.0", "fast-fuzzy": "^1.12.0",
"file-type-mime": "^0.4.3", "file-type-mime": "^0.4.3",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
"lru-cache": "^11.1.0",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"micromark": "^4.0.1", "micromark": "^4.0.1",
"nuxt": "^3.16.2", "nuxt": "^3.16.2",
@ -40,6 +39,7 @@
"sharp": "^0.33.5", "sharp": "^0.33.5",
"stream-mime-type": "^2.0.0", "stream-mime-type": "^2.0.0",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"unstorage": "^1.15.0",
"vue": "latest", "vue": "latest",
"vue-router": "latest", "vue-router": "latest",
"vue3-carousel": "^0.15.0", "vue3-carousel": "^0.15.0",
@ -75,4 +75,4 @@
"prisma": { "prisma": {
"schema": "./prisma" "schema": "./prisma"
} }
} }

33
server/internal/cache/cacheHandler.ts vendored Normal file
View File

@ -0,0 +1,33 @@
import { prefixStorage, type StorageValue, type Storage } from "unstorage";
export interface CacheProviderOptions {
/**
* Max number of items in the cache
*/
max?: number;
/**
* Time to live (in ms)
*/
ttl?: number;
}
/**
* Creates and manages the lifecycles of various caches
*/
export class CacheHandler {
private caches = new Map<string, Storage<StorageValue>>();
/**
* Create a new cache
* @param name
* @returns
*/
createCache<V extends StorageValue>(name: string) {
// will allow us to dynamicing use redis in the future just by changing the storage used
const provider = prefixStorage<V>(useStorage<V>("appCache"), name);
// hack to let ts have us store cache
this.caches.set(name, provider as unknown as Storage<StorageValue>);
return provider;
}
}

4
server/internal/cache/index.ts vendored Normal file
View File

@ -0,0 +1,4 @@
import { CacheHandler } from "./cacheHandler";
export const cacheHandler = new CacheHandler();
export default cacheHandler;

View File

@ -1,12 +1,12 @@
import type { ObjectMetadata, ObjectReference, Source } from "./objectHandler"; import type { ObjectMetadata, ObjectReference, Source } from "./objectHandler";
import { ObjectBackend } from "./objectHandler"; import { ObjectBackend } from "./objectHandler";
import { LRUCache } from "lru-cache";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { Readable } from "stream"; import { Readable } from "stream";
import { createHash } from "crypto"; import { createHash } from "crypto";
import prisma from "../db/database"; import prisma from "../db/database";
import cacheHandler from "../cache";
export class FsObjectBackend extends ObjectBackend { export class FsObjectBackend extends ObjectBackend {
private baseObjectPath: string; private baseObjectPath: string;
@ -34,7 +34,7 @@ export class FsObjectBackend extends ObjectBackend {
if (!fs.existsSync(objectPath)) return false; if (!fs.existsSync(objectPath)) return false;
// remove item from cache // remove item from cache
this.hashStore.delete(id); await this.hashStore.delete(id);
if (source instanceof Readable) { if (source instanceof Readable) {
const outputStream = fs.createWriteStream(objectPath); const outputStream = fs.createWriteStream(objectPath);
@ -54,7 +54,7 @@ export class FsObjectBackend extends ObjectBackend {
const objectPath = path.join(this.baseObjectPath, id); const objectPath = path.join(this.baseObjectPath, id);
if (!fs.existsSync(objectPath)) return undefined; if (!fs.existsSync(objectPath)) return undefined;
// remove item from cache // remove item from cache
this.hashStore.delete(id); await this.hashStore.delete(id);
return fs.createWriteStream(objectPath); return fs.createWriteStream(objectPath);
} }
async create( async create(
@ -99,7 +99,7 @@ export class FsObjectBackend extends ObjectBackend {
if (!fs.existsSync(objectPath)) return true; if (!fs.existsSync(objectPath)) return true;
fs.rmSync(objectPath); fs.rmSync(objectPath);
// remove item from cache // remove item from cache
this.hashStore.delete(id); await this.hashStore.delete(id);
return true; return true;
} }
async fetchMetadata( async fetchMetadata(
@ -121,36 +121,35 @@ export class FsObjectBackend extends ObjectBackend {
} }
async fetchHash(id: ObjectReference): Promise<string | undefined> { async fetchHash(id: ObjectReference): Promise<string | undefined> {
const cacheResult = await this.hashStore.get(id); const cacheResult = await this.hashStore.get(id);
if (cacheResult !== undefined) return cacheResult; if (cacheResult !== null) return cacheResult;
const obj = await this.fetch(id); const obj = await this.fetch(id);
if (obj === undefined) return; if (obj === undefined) return;
// local variable to point to object
const cache = this.hashStore;
// hash object // hash object
const hash = createHash("md5"); const hash = createHash("md5");
hash.setEncoding("hex"); hash.setEncoding("hex");
// local variable to point to object
const store = this.hashStore;
// read obj into hash // read obj into hash
obj.pipe(hash); obj.pipe(hash);
await new Promise<void>((r) => { await new Promise<void>((r) => {
obj.on("end", function () { obj.on("end", async function () {
hash.end(); hash.end();
cache.save(id, hash.read()); await store.save(id, hash.read());
r(); r();
}); });
}); });
return await this.hashStore.get(id); const result = await this.hashStore.get(id);
return result === null ? undefined : result;
} }
} }
class FsHashStore { class FsHashStore {
private cache = new LRUCache<string, string>({ private cache = cacheHandler.createCache<string>("ObjectHashStore");
max: 1000, // number of items
});
/** /**
* Gets hash of object * Gets hash of object
@ -158,8 +157,11 @@ class FsHashStore {
* @returns * @returns
*/ */
async get(id: ObjectReference) { async get(id: ObjectReference) {
const cacheRes = this.cache.get(id); const cacheRes = await this.cache.get(id);
if (cacheRes !== undefined) return cacheRes; if (cacheRes !== null) {
console.log("object cache hit");
return cacheRes;
}
const objectHash = await prisma.objectHash.findUnique({ const objectHash = await prisma.objectHash.findUnique({
where: { where: {
@ -170,7 +172,7 @@ class FsHashStore {
}, },
}); });
if (objectHash === null) return undefined; if (objectHash === null) return undefined;
this.cache.set(id, objectHash.hash); await this.cache.set(id, objectHash.hash);
return objectHash.hash; return objectHash.hash;
} }
@ -191,7 +193,7 @@ class FsHashStore {
hash, hash,
}, },
}); });
this.cache.set(id, hash); await this.cache.set(id, hash);
} }
/** /**
@ -199,7 +201,7 @@ class FsHashStore {
* @param id * @param id
*/ */
async delete(id: ObjectReference) { async delete(id: ObjectReference) {
this.cache.delete(id); await this.cache.remove(id);
try { try {
// need to catch in case the object doesn't exist // need to catch in case the object doesn't exist

View File

@ -1,16 +1,13 @@
import { LRUCache } from "lru-cache";
import prisma from "../db/database"; import prisma from "../db/database";
import type { Session, SessionProvider } from "./types"; import type { Session, SessionProvider } from "./types";
import cacheHandler from "../cache";
export default function createDBSessionHandler(): SessionProvider { export default function createDBSessionHandler(): SessionProvider {
const cache = new LRUCache<string, Session>({ const cache = cacheHandler.createCache<Session>("DBSession");
max: 50, // number of items
ttl: 30 * 100, // 30s (in ms)
});
return { return {
async setSession(token, session) { async setSession(token, session) {
cache.set(token, session); await cache.set(token, session);
// const strData = JSON.stringify(data); // const strData = JSON.stringify(data);
await prisma.session.upsert({ await prisma.session.upsert({
@ -29,8 +26,8 @@ export default function createDBSessionHandler(): SessionProvider {
return await this.setSession(token, data); return await this.setSession(token, data);
}, },
async getSession<T extends Session>(token: string) { async getSession<T extends Session>(token: string) {
const cached = cache.get(token); const cached = await cache.get(token);
if (cached !== undefined) return cached as T; if (cached !== null) return cached as T;
const result = await prisma.session.findUnique({ const result = await prisma.session.findUnique({
where: { where: {
@ -45,7 +42,7 @@ export default function createDBSessionHandler(): SessionProvider {
return result as unknown as T; return result as unknown as T;
}, },
async removeSession(token) { async removeSession(token) {
cache.delete(token); await cache.remove(token);
await prisma.session.delete({ await prisma.session.delete({
where: { where: {
token, token,

View File

@ -2,15 +2,17 @@
Handles managing collections Handles managing collections
*/ */
import cacheHandler from "../cache";
import prisma from "../db/database"; import prisma from "../db/database";
class UserLibraryManager { class UserLibraryManager {
// Caches the user's core library // Caches the user's core library
private userCoreLibraryCache: { [key: string]: string } = {}; private coreLibraryCache =
cacheHandler.createCache<string>("UserCoreLibrary");
private async fetchUserLibrary(userId: string) { private async fetchUserLibrary(userId: string) {
if (this.userCoreLibraryCache[userId]) const cached = await this.coreLibraryCache.get(userId);
return this.userCoreLibraryCache[userId]; if (cached !== null) return cached;
let collection = await prisma.collection.findFirst({ let collection = await prisma.collection.findFirst({
where: { where: {
@ -28,7 +30,7 @@ class UserLibraryManager {
}, },
}); });
this.userCoreLibraryCache[userId] = collection.id; await this.coreLibraryCache.set(userId, collection.id);
return collection.id; return collection.id;
} }