feat: correctly cache images with redis
This commit is contained in:
29
lib/cache.ts
29
lib/cache.ts
@ -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
42
lib/cache/cache.ts
vendored
Normal 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
57
lib/cache/documents.ts
vendored
Normal 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
63
lib/cache/image.ts
vendored
Normal 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,
|
||||
}),
|
||||
);
|
||||
}
|
@ -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
9
lib/hash.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
Reference in New Issue
Block a user