From 01d91e547789c086b4db7763e16be607cd049aa0 Mon Sep 17 00:00:00 2001 From: vighnesh153 Date: Thu, 2 Jan 2025 08:31:56 +0530 Subject: [PATCH] feat: add image caching --- GUIDE_AND_TRACKER.md | 11 +++ .../website/package-lock.json | 1 + .../vighnesh153-astro/website/package.json | 1 + .../website/src/components/ImageWithCache.tsx | 33 +++++++ .../private/private_cards_collection.tsx | 65 +++++++------- .../src/components/private/private_stub.tsx | 37 ++++---- .../website/src/layouts/BaseLayout.astro | 2 + .../website/src/pages/playground.astro | 19 ++++ .../website/src/utils/image_caching.ts | 86 +++++++++++++++++++ 9 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 tools-nodejs/vighnesh153-astro/website/src/components/ImageWithCache.tsx create mode 100644 tools-nodejs/vighnesh153-astro/website/src/utils/image_caching.ts diff --git a/GUIDE_AND_TRACKER.md b/GUIDE_AND_TRACKER.md index 88aa3faf..69cb78ad 100644 --- a/GUIDE_AND_TRACKER.md +++ b/GUIDE_AND_TRACKER.md @@ -12,7 +12,18 @@ #### Tasks +- caching images in indexed db +- todo: uncomment other images from private_stub.tsx +- resume as code - Blog: Why i moved away from react and next js +- Users page +- User profile page +- Change username + - Old username should exist for 7 days (add a expiresAt field of the username + record) + - Run a cron job daily (or weekly) that deletes the expired usernames + - Old username would be available for reuse once it has been cleared from the + db by the cron job - Create Kotlin AST - Kotlin Syntax highlighting - Differ between 2 string content diff --git a/tools-nodejs/vighnesh153-astro/website/package-lock.json b/tools-nodejs/vighnesh153-astro/website/package-lock.json index 967deca4..6f72c9f0 100644 --- a/tools-nodejs/vighnesh153-astro/website/package-lock.json +++ b/tools-nodejs/vighnesh153-astro/website/package-lock.json @@ -20,6 +20,7 @@ "@vighnesh153/tools-browser": "npm:@jsr/vighnesh153__tools-browser@0.1.2", "@vighnesh153/tsx-bundler": "0.4.8", "firebase": "^11.1.0", + "localforage": "^1.10.0", "localstorage-slim": "^2.7.1", "nanostores": "^0.11.3", "solid-js": "^1.9.3", diff --git a/tools-nodejs/vighnesh153-astro/website/package.json b/tools-nodejs/vighnesh153-astro/website/package.json index b6f7f03d..b98ce939 100644 --- a/tools-nodejs/vighnesh153-astro/website/package.json +++ b/tools-nodejs/vighnesh153-astro/website/package.json @@ -32,6 +32,7 @@ "@vighnesh153/tools-browser": "npm:@jsr/vighnesh153__tools-browser@0.1.2", "@vighnesh153/tsx-bundler": "0.4.8", "firebase": "^11.1.0", + "localforage": "^1.10.0", "localstorage-slim": "^2.7.1", "nanostores": "^0.11.3", "solid-js": "^1.9.3", diff --git a/tools-nodejs/vighnesh153-astro/website/src/components/ImageWithCache.tsx b/tools-nodejs/vighnesh153-astro/website/src/components/ImageWithCache.tsx new file mode 100644 index 00000000..e5902614 --- /dev/null +++ b/tools-nodejs/vighnesh153-astro/website/src/components/ImageWithCache.tsx @@ -0,0 +1,33 @@ +import { createEffect, createSignal, type JSX, Show } from "solid-js"; + +import { cacheImage } from "@/utils/image_caching.ts"; + +export type ImageWithCacheProps = { + src: string; + cacheKey: string; + ttlMillis?: number; + fallback?: JSX.Element; + + imageProps?: JSX.ImgHTMLAttributes; +}; + +export function ImageWithCache(props: ImageWithCacheProps): JSX.Element { + const [objUrl, setObjUrl] = createSignal(null); + + createEffect(async () => { + try { + const cachedUrl = await cacheImage(props.src, props.cacheKey, { + ttlMillis: props.ttlMillis, + }); + setObjUrl(cachedUrl); + } catch (e) { + console.log(e); + } + }); + + return ( + + + + ); +} diff --git a/tools-nodejs/vighnesh153-astro/website/src/components/private/private_cards_collection.tsx b/tools-nodejs/vighnesh153-astro/website/src/components/private/private_cards_collection.tsx index 5dcda580..b3a06a35 100644 --- a/tools-nodejs/vighnesh153-astro/website/src/components/private/private_cards_collection.tsx +++ b/tools-nodejs/vighnesh153-astro/website/src/components/private/private_cards_collection.tsx @@ -4,6 +4,7 @@ import { classes, internalLinks } from "@/utils"; import { clearPrivateContentFromCache } from "@/store/private_content"; import { Button } from "@/components/buttons"; +import { ImageWithCache } from "@/components/ImageWithCache.tsx"; import { usePrivateContent } from "./usePrivateContent"; export function PrivateCardsCollection(): JSX.Element { @@ -40,35 +41,41 @@ export function PrivateCardsCollection(): JSX.Element { `)} > - {(card) => ( - - private content - - )} + {(card) => { + const imageUrl = new URL(card.imageUrl); + return ( + + + + ); + }} diff --git a/tools-nodejs/vighnesh153-astro/website/src/components/private/private_stub.tsx b/tools-nodejs/vighnesh153-astro/website/src/components/private/private_stub.tsx index 527e3fbc..5edf1532 100644 --- a/tools-nodejs/vighnesh153-astro/website/src/components/private/private_stub.tsx +++ b/tools-nodejs/vighnesh153-astro/website/src/components/private/private_stub.tsx @@ -1,6 +1,7 @@ import { For, type JSX, Show } from "solid-js"; import { not } from "@vighnesh153/tools"; +import { ImageWithCache } from "@/components/ImageWithCache.tsx"; import { useAccidentalPrivatePageOpenStubBreaker } from "@/hooks/mod.ts"; import { PrivateContentWrapper } from "./private_content_wrapper"; @@ -9,14 +10,14 @@ const prefix = const images = [ "adorable-ragdoll-cat-beautiful-blue-600nw-1992318023.webp?alt=media", - "adult-fluffy-ragdoll-cat-outside-600nw-1467863648.webp?alt=media", - "cute-gray-cat-kitten-looking-600nw-2386006989.webp?alt=media", - "happy-dog-running-blossoming-flower-600nw-2458562695.webp?alt=media", - "happy-labrador-dog-water-splashes-600nw-2470403137.webp?alt=media", - "savannah-cat-beautiful-spotted-striped-600nw-589366805.webp?alt=media", - "sphynx-cat-naked-600nw-1058785700.webp?alt=media", - "tabby-cat-169-ratio-600nw-44442067.webp?alt=media", - "young-cute-bengal-cat-sitting-600nw-2238825697.webp?alt=media", + // "adult-fluffy-ragdoll-cat-outside-600nw-1467863648.webp?alt=media", + // "cute-gray-cat-kitten-looking-600nw-2386006989.webp?alt=media", + // "happy-dog-running-blossoming-flower-600nw-2458562695.webp?alt=media", + // "happy-labrador-dog-water-splashes-600nw-2470403137.webp?alt=media", + // "savannah-cat-beautiful-spotted-striped-600nw-589366805.webp?alt=media", + // "sphynx-cat-naked-600nw-1058785700.webp?alt=media", + // "tabby-cat-169-ratio-600nw-44442067.webp?alt=media", + // "young-cute-bengal-cat-sitting-600nw-2238825697.webp?alt=media", ]; export function PrivateStub(): JSX.Element { @@ -28,13 +29,19 @@ export function PrivateStub(): JSX.Element { }>
- {(p, index) => ( - onKey((index() + 1).toString())} - /> - )} + {(p, index) => { + const url = new URL(prefix + p); + return ( + onKey((index() + 1).toString()), + }} + /> + ); + }}
diff --git a/tools-nodejs/vighnesh153-astro/website/src/layouts/BaseLayout.astro b/tools-nodejs/vighnesh153-astro/website/src/layouts/BaseLayout.astro index ed59491e..d137527a 100644 --- a/tools-nodejs/vighnesh153-astro/website/src/layouts/BaseLayout.astro +++ b/tools-nodejs/vighnesh153-astro/website/src/layouts/BaseLayout.astro @@ -42,8 +42,10 @@ const { title, description } = Astro.props; diff --git a/tools-nodejs/vighnesh153-astro/website/src/pages/playground.astro b/tools-nodejs/vighnesh153-astro/website/src/pages/playground.astro index 70ecc410..046d3722 100644 --- a/tools-nodejs/vighnesh153-astro/website/src/pages/playground.astro +++ b/tools-nodejs/vighnesh153-astro/website/src/pages/playground.astro @@ -3,6 +3,7 @@ import ContentLayout from "@/layouts/ContentLayout.astro"; import { UploadInputBoxWithStats } from "@/components/uploader"; // import { CodeViewer } from "@/components/code_viewer/index.ts"; import { classes } from "@/utils/index.ts"; +import { Button } from "@/components/buttons"; --- --> + + + + diff --git a/tools-nodejs/vighnesh153-astro/website/src/utils/image_caching.ts b/tools-nodejs/vighnesh153-astro/website/src/utils/image_caching.ts new file mode 100644 index 00000000..23e55f19 --- /dev/null +++ b/tools-nodejs/vighnesh153-astro/website/src/utils/image_caching.ts @@ -0,0 +1,86 @@ +import { milliseconds } from "@vighnesh153/tools"; +import localforage from "localforage"; + +const defaultCacheTtl = milliseconds({ years: 1 }); + +const imageStore = localforage.createInstance({ + name: "vighnesh153-images", +}); + +const imageTtlStore = localforage.createInstance({ + name: "vighnesh153-images-ttl", +}); + +export type CacheImageOptions = { + ttlMillis?: number; +}; + +export async function cacheImage( + networkUri: string, + cacheKey: string, + { ttlMillis = defaultCacheTtl }: CacheImageOptions = {}, +): Promise { + return navigator.locks.request(`images-${cacheKey}`, async () => { + const existingImage = await getImageObjUrl(cacheKey); + if (existingImage !== null) { + return existingImage; + } + + return new Promise(async (resolve, reject) => { + const blob = await fetch(networkUri).then((res) => res.blob()); + + const reader = new FileReader(); + reader.onload = () => { + const objectUrl = reader.result as string; + + imageStore.setItem(cacheKey, objectUrl).then(async () => { + resolve(objectUrl); + await imageTtlStore.setItem(cacheKey, Date.now() + ttlMillis); + }); + }; + + try { + reader.readAsDataURL(blob); + } catch (e) { + reject(e); + } + }); + }); +} + +async function getImageObjUrl(cacheKey: string): Promise { + const ttl = await imageTtlStore.getItem(cacheKey); + if (typeof ttl !== "number") { + // either ttl doesn't exist or is corrupted for this cache key + return null; + } + + if (ttl < Date.now()) { + // Cache expired + return null; + } + + const maybeImageObjUrl = await imageStore.getItem(cacheKey); + if (typeof maybeImageObjUrl === "string") { + return maybeImageObjUrl; + } + + // imageObjUrl is corrupted + return null; +} + +export async function cleanupExpiredImages() { + const imageKeys = await imageStore.keys(); + const ttlKeys = await imageTtlStore.keys(); + + for (const key of new Set([...imageKeys, ...ttlKeys])) { + const objUrl = await imageStore.getItem(key); + const ttl = await imageTtlStore.getItem(key); + if ( + typeof objUrl !== "string" || typeof ttl !== "number" || ttl < Date.now() + ) { + await imageStore.removeItem(key); + await imageTtlStore.removeItem(key); + } + } +}