feat: correctly cache images with redis

This commit is contained in:
2023-07-30 18:27:45 +02:00
parent 1917fc7d8f
commit af8adf9ce7
17 changed files with 321 additions and 121 deletions

View File

@ -1,29 +0,0 @@
import { connect } from "https://deno.land/x/redis/mod.ts";
const REDIS_HOST = Deno.env.get("REDIS_HOST");
const REDIS_PASS = Deno.env.get("REDIS_PASS");
const REDIS_PORT = Deno.env.get("REDIS_PORT");
async function createCache() {
if (REDIS_HOST && REDIS_PASS) {
const client = await connect({
password: REDIS_PASS,
hostname: REDIS_HOST,
port: REDIS_PORT || 6379,
});
console.log("COnnected to redis");
return client;
}
return new Map<string, any>();
}
const cache = await createCache();
export async function get(id: string) {
return await cache.get(id);
}
export async function set(id: string, content: any) {
return await cache.set(id, content);
}

42
lib/cache/cache.ts vendored Normal file
View File

@ -0,0 +1,42 @@
import {
connect,
Redis,
RedisConnectOptions,
RedisValue,
} from "https://deno.land/x/redis@v0.31.0/mod.ts";
const REDIS_HOST = Deno.env.get("REDIS_HOST");
const REDIS_PASS = Deno.env.get("REDIS_PASS") || "";
const REDIS_PORT = Deno.env.get("REDIS_PORT");
async function createCache<T>(): Promise<Map<string, T> | Redis> {
if (REDIS_HOST) {
const conf: RedisConnectOptions = {
hostname: REDIS_HOST,
port: REDIS_PORT || 6379,
};
if (REDIS_PASS) {
conf.password = REDIS_PASS;
}
const client = await connect(conf);
console.log("Connected to redis");
return client;
}
return new Map<string, T>();
}
const cache = await createCache();
export async function get<T>(id: string, binary = false) {
if (binary && !(cache instanceof Map)) {
return await cache.sendCommand("GET", [id], {
returnUint8Arrays: true,
}) as T;
}
return await cache.get(id) as T;
}
export async function set<T extends RedisValue>(id: string, content: T) {
return await cache.set(id, content);
}

57
lib/cache/documents.ts vendored Normal file
View File

@ -0,0 +1,57 @@
import { Document } from "@lib/documents.ts";
import * as cache from "@lib/cache/cache.ts";
type DocumentsCache = {
lastUpdated: number;
documents: Document[];
};
const CACHE_INTERVAL = 5000; // 5 seconds;
const CACHE_KEY = "documents";
export async function getDocuments() {
const docs = await cache.get<DocumentsCache>(CACHE_KEY);
if (!docs) return;
if (Date.now() > docs.lastUpdated + CACHE_INTERVAL) {
return;
}
return docs.documents;
}
export function setDocuments(documents: Document[]) {
return cache.set(
CACHE_KEY,
JSON.stringify({
lastUpdated: Date.now(),
documents,
}),
);
}
type DocumentCache = {
lastUpdated: number;
content: string;
};
export async function getDocument(id: string) {
const doc = await cache.get<DocumentCache>(CACHE_KEY + "/" + id);
if (!doc) return;
if (Date.now() > doc.lastUpdated + CACHE_INTERVAL) {
return;
}
return doc.content;
}
export async function setDocument(id: string, content: string) {
await cache.set(
CACHE_KEY + "/" + id,
JSON.stringify({
lastUpdated: Date.now(),
content,
}),
);
}

63
lib/cache/image.ts vendored Normal file
View File

@ -0,0 +1,63 @@
import { hash } from "@lib/hash.ts";
import * as cache from "@lib/cache/cache.ts";
type ImageCacheOptions = {
url: string;
width: number;
height: number;
mediaType?: string;
};
const CACHE_KEY = "images";
function getCacheKey({ url: _url, width, height }: ImageCacheOptions) {
const url = new URL(_url);
return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}`
.replace(
"//",
"/",
);
}
export async function getImage({ url, width, height }: ImageCacheOptions) {
const cacheKey = getCacheKey({ url, width, height });
const pointerCacheRaw = await cache.get<string>(cacheKey);
if (!pointerCacheRaw) return;
const pointerCache = typeof pointerCacheRaw === "string"
? JSON.parse(pointerCacheRaw)
: pointerCacheRaw;
const imageContent = await cache.get(pointerCache.id, true);
if (!imageContent) return;
return {
...pointerCache,
buffer: imageContent,
};
}
export async function setImage(
buffer: Uint8Array,
{ url, width, height, mediaType }: ImageCacheOptions,
) {
const clone = new Uint8Array(buffer);
const cacheKey = getCacheKey({ url, width, height });
const pointerId = await hash(cacheKey);
await cache.set(pointerId, clone);
await cache.set(
cacheKey,
JSON.stringify({
id: pointerId,
url,
width,
height,
mediaType,
}),
);
}

View File

@ -2,6 +2,7 @@ import { unified } from "npm:unified";
import remarkParse from "npm:remark-parse";
import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4";
import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts";
import * as cache from "@lib/cache/documents.ts";
const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
@ -18,6 +19,9 @@ export function parseFrontmatter(yaml: string) {
}
export async function getDocuments(): Promise<Document[]> {
const cachedDocuments = await cache.getDocuments();
if (cachedDocuments) return cachedDocuments;
const headers = new Headers();
headers.append("Accept", "application/json");
@ -25,12 +29,22 @@ export async function getDocuments(): Promise<Document[]> {
headers: headers,
});
return response.json();
const documents = await response.json();
cache.setDocuments(documents);
return documents;
}
export async function getDocument(name: string): Promise<string> {
const cachedDocument = await cache.getDocument(name);
if (cachedDocument) return cachedDocument;
const response = await fetch(SILVERBULLET_SERVER + "/" + name);
return await response.text();
const text = await response.text();
cache.setDocument(name, text);
return text;
}
export function parseDocument(doc: string) {

9
lib/hash.ts Normal file
View File

@ -0,0 +1,9 @@
export async function hash(message: string) {
const data = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
"",
);
return hashHex;
}

View File

@ -4,7 +4,7 @@ import {
getTextOfRange,
parseDocument,
parseFrontmatter,
} from "./documents.ts";
} from "@lib/documents.ts";
import { parseIngredient } from "npm:parse-ingredient";
@ -161,10 +161,7 @@ export function parseRecipe(original: string, id: string): Recipe {
return {
id,
meta: {
...meta,
image: meta?.image?.replace(/^Recipes\/images/, "/api/recipes/images"),
},
meta,
name,
description,
ingredients,