Skip to content

Commit

Permalink
feat: add image caching
Browse files Browse the repository at this point in the history
  • Loading branch information
vighnesh153 committed Jan 2, 2025
1 parent f7e8113 commit 01d91e5
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 44 deletions.
11 changes: 11 additions & 0 deletions GUIDE_AND_TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tools-nodejs/vighnesh153-astro/website/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tools-nodejs/vighnesh153-astro/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLImageElement>;
};

export function ImageWithCache(props: ImageWithCacheProps): JSX.Element {
const [objUrl, setObjUrl] = createSignal<string | null>(null);

createEffect(async () => {
try {
const cachedUrl = await cacheImage(props.src, props.cacheKey, {
ttlMillis: props.ttlMillis,
});
setObjUrl(cachedUrl);
} catch (e) {
console.log(e);
}
});

return (
<Show when={objUrl() !== null} fallback={props.fallback}>
<img {...props.imageProps} src={objUrl()!} />
</Show>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -40,35 +41,41 @@ export function PrivateCardsCollection(): JSX.Element {
`)}
>
<For each={privateContent()?.data ?? []}>
{(card) => (
<a
class={classes(`
min-w-5
hover:scale-105
focus-visible:scale-105
cursor-pointer
transition-all
bg-primary
rounded-lg
aspect-video
overflow-hidden
`)}
href={internalLinks.private.buildPrivateContentLinkFromId(
card.id,
)}
>
<img
src={card.imageUrl}
alt="private content"
class="block w-full h-full"
loading="lazy"
/>
</a>
)}
{(card) => {
const imageUrl = new URL(card.imageUrl);
return (
<a
class={classes(`
min-w-5
hover:scale-105
focus-visible:scale-105
cursor-pointer
transition-all
bg-primary
rounded-lg
aspect-video
overflow-hidden
`)}
href={internalLinks.private.buildPrivateContentLinkFromId(
card.id,
)}
>
<ImageWithCache
src={imageUrl.toString()}
cacheKey={imageUrl.pathname}
imageProps={{
alt: "private content",
class: "block w-full h-full",
loading: "lazy",
}}
/>
</a>
);
}}
</For>
</div>
</Show>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 {
Expand All @@ -28,13 +29,19 @@ export function PrivateStub(): JSX.Element {
<Show when={not(pageAccessible())} fallback={<PrivateContentWrapper />}>
<div class="grid columns-1 gap-6">
<For each={images}>
{(p, index) => (
<img
class="block w-full"
src={prefix + p}
onClick={() => onKey((index() + 1).toString())}
/>
)}
{(p, index) => {
const url = new URL(prefix + p);
return (
<ImageWithCache
src={url.toString()}
cacheKey={url.pathname}
imageProps={{
class: "block w-full",
onClick: () => onKey((index() + 1).toString()),
}}
/>
);
}}
</For>
</div>
</Show>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ const { title, description } = Astro.props;

<script>
import { initializeUserInStore } from "@/store/auth.ts";
import { cleanupExpiredImages } from "@/utils/image_caching";
import { setupLocalStorageCleaner } from "@/utils/local_storage";

setupLocalStorageCleaner();
await initializeUserInStore();
await cleanupExpiredImages();
</script>
19 changes: 19 additions & 0 deletions tools-nodejs/vighnesh153-astro/website/src/pages/playground.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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";
---

<ContentLayout
Expand Down Expand Up @@ -97,3 +98,21 @@ fun formatXml(
logUrl("Public", "public/pokemon.jpeg"),
]);
</script> -->

<Button class="hello">Share test</Button>

<script>
document
.querySelector("button.hello")
?.addEventListener("click", async () => {
try {
await navigator.share({
title: "MDN",
text: "Learn web development on MDN!",
url: "https://developer.mozilla.org",
});
} catch (e) {
alert((e as any)?.message);
}
});
</script>
86 changes: 86 additions & 0 deletions tools-nodejs/vighnesh153-astro/website/src/utils/image_caching.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string | null> {
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);
}
}
}

0 comments on commit 01d91e5

Please sign in to comment.