My landing page, written in Astro hayden.moe
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: post caching with cf kv

+80 -15
+2 -2
src/app/routes/posts.$rkey.tsx
··· 5 5 import { FormattedDate } from '../components/formatted-date'; 6 6 import Markdown from 'react-markdown'; 7 7 8 - export const loader = async ({ params }: LoaderFunctionArgs) => { 8 + export const loader = async ({ params, context }: LoaderFunctionArgs) => { 9 9 const { rkey } = params; 10 - const post = await getPost(rkey!); 10 + const post = await getPost(context, rkey!); 11 11 return json({ post }); 12 12 }; 13 13
+3 -3
src/app/routes/posts._index.tsx
··· 1 - import { json } from '@remix-run/cloudflare'; 1 + import { json, LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 2 import { useLoaderData } from '@remix-run/react'; 3 3 import { getPosts } from 'src/atproto/getPosts'; 4 4 import { WhtwndBlogEntryView } from 'src/types'; 5 5 import { FormattedDate } from '../components/formatted-date'; 6 6 import Markdown from 'react-markdown'; 7 7 8 - export const loader = async () => { 9 - const posts = await getPosts(undefined); 8 + export const loader = async ({ context }: LoaderFunctionArgs) => { 9 + const posts = await getPosts(context, undefined); 10 10 11 11 const postsFiltered = posts.filter(p => !p.content?.startsWith('NOT_LIVE')); 12 12
+4 -3
src/atproto/agent.ts
··· 1 1 import { AtpAgent } from '@atproto/api'; 2 + import { AppLoadContext } from '@remix-run/cloudflare'; 2 3 3 - export const atpAgent = new AtpAgent({ 4 - service: process.env.ATP_SERVICE!, 5 - }) 4 + export const atpAgent = (ctx: AppLoadContext) => new AtpAgent({ 5 + service: ctx.cloudflare.env.ATP_SERVICE, 6 + });
+12 -3
src/atproto/getPost.ts
··· 1 1 import { WhtwndBlogEntryRecord, WhtwndBlogEntryView } from "src/types"; 2 2 import { atpAgent } from "./agent"; 3 3 import { whtwndBlogEntryRecordToView } from "./dataToView"; 4 + import { AppLoadContext } from "@remix-run/cloudflare"; 5 + import { getCachedPost, setCachedPost } from "src/kv"; 4 6 5 - export const getPost = async (rkey: string, skipCache?: boolean) => { 6 - const repo = process.env.ATP_IDENTIFIER!; 7 + export const getPost = async (ctx: AppLoadContext, rkey: string, skipCache?: boolean) => { 8 + const cachedRes = await getCachedPost(ctx, rkey); 9 + if (!skipCache && cachedRes) { 10 + console.log('cache hit!'); 11 + return cachedRes; 12 + } 13 + console.log('cache not hit!'); 7 14 8 - const res = await atpAgent.com.atproto.repo.getRecord({ 15 + const repo = ctx.cloudflare.env.ATP_IDENTIFIER; 16 + const res = await atpAgent(ctx).com.atproto.repo.getRecord({ 9 17 collection: 'com.whtwnd.blog.entry', 10 18 repo, 11 19 rkey, ··· 21 29 value: res.data.value as WhtwndBlogEntryRecord, 22 30 }) as WhtwndBlogEntryView; 23 31 32 + await setCachedPost(ctx, post); 24 33 return post; 25 34 };
+15 -3
src/atproto/getPosts.ts
··· 1 1 import { WhtwndBlogEntryRecord, WhtwndBlogEntryView } from "src/types"; 2 2 import { atpAgent } from "./agent"; 3 3 import { whtwndBlogEntryRecordToView } from "./dataToView"; 4 + import { AppLoadContext } from "@remix-run/cloudflare"; 5 + import { getCachedPosts, setCachedPost, setCachedPosts } from "src/kv"; 4 6 5 - export const getPosts = async (cursor: string | undefined, skipCache?: boolean) => { 6 - const repo = process.env.ATP_IDENTIFIER!; 7 - const res = await atpAgent.com.atproto.repo.listRecords({ 7 + export const getPosts = async (ctx: AppLoadContext, cursor: string | undefined, skipCache?: boolean) => { 8 + const cachedRes = await getCachedPosts(ctx); 9 + if (!skipCache && cachedRes) { 10 + return cachedRes; 11 + } 12 + 13 + const repo = ctx.cloudflare.env.ATP_IDENTIFIER; 14 + const res = await atpAgent(ctx).com.atproto.repo.listRecords({ 8 15 collection: 'com.whtwnd.blog.entry', 9 16 repo, 10 17 cursor, ··· 19 26 cid: data.cid?.toString() ?? '', 20 27 value: data.value as WhtwndBlogEntryRecord, 21 28 })) as WhtwndBlogEntryView[]; 29 + 30 + for (const post of posts) { 31 + await setCachedPost(ctx, post); 32 + } 33 + await setCachedPosts(ctx, posts); 22 34 23 35 return posts; 24 36 }
+34
src/kv/index.ts
··· 1 + import { AppLoadContext } from "@remix-run/cloudflare"; 2 + import { WhtwndBlogEntryView } from "src/types"; 3 + 4 + export const getCachedPosts = async (context: AppLoadContext) => { 5 + const res = await context.cloudflare.env.CACHE.get('post:all', 'json'); 6 + if (!res) { 7 + return null; 8 + } 9 + return res as WhtwndBlogEntryView[]; 10 + }; 11 + 12 + export const setCachedPosts = async (context: AppLoadContext, posts: WhtwndBlogEntryView[]) => { 13 + await context.cloudflare.env.CACHE.put( 14 + 'post:all', 15 + JSON.stringify(posts), 16 + { expirationTtl: 60 }, 17 + ); 18 + }; 19 + 20 + export const getCachedPost = async (context: AppLoadContext, rkey: string) => { 21 + const res = await context.cloudflare.env.CACHE.get(`post:${rkey}`, 'json'); 22 + if (!res) { 23 + return null; 24 + } 25 + return res as WhtwndBlogEntryView; 26 + }; 27 + 28 + export const setCachedPost = async (context: AppLoadContext, post: WhtwndBlogEntryView) => { 29 + await context.cloudflare.env.CACHE.put( 30 + `post:${post.rkey}`, 31 + JSON.stringify(post), 32 + { expirationTtl: 60 * 10 }, 33 + ); 34 + };
+6 -1
worker-configuration.d.ts
··· 1 1 // Generated by Wrangler by running `wrangler types` 2 2 3 - interface Env {} 3 + interface Env { 4 + CACHE: KVNamespace; 5 + ATP_SERVICE: string; 6 + ATP_IDENTIFIER: string; 7 + ATP_DID: string; 8 + }
+4
wrangler.toml
··· 10 10 11 11 [build] 12 12 command = "npm run build" 13 + 14 + [[kv_namespaces]] 15 + binding = "CACHE" 16 + id = "hbjy_kv_atpblog"