mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-10-27 02:16:23 -04:00
refactor: use "use cache: remote" for request-time reads and updateTag/refresh for mutations
- Replace `"use cache"` with `"use cache: remote"` on functions that run inside dynamic holes (`getContributions`, `getRepos`, `getPostStats`, `getCachedComments`) so entries are shared across server instances
- Switch comment and view server actions from `revalidatePath`/`revalidateTag` to `updateTag` + `refresh` for read-your-writes invalidation
- Extract a `commentsTag(pageSlug)` helper so each page's comment cache is expired independently instead of blowing the whole "comments" tag
- Refactor `getComments` to delegate to a cached inner function that throws on failure (so errors are never cached); `getCommentCount` now reuses that cache entry instead of issuing a separate COUNT query
- Drop `lib/data/views.ts` (`getAllViewCounts`) — the `/api/hits` route now calls `getPostStats()` instead
- Remove `revalidateTag("views")` from `incrementViews`; view counts in `getPostStats` are allowed to lag by a cache TTL
- Remove the `Suspense`/`Skeleton` wrapper on the projects page since `getContributions` is now a remote-cached call
This commit is contained in:
10 files changed
+75
-96
No files matched your search
+39
-35
@@ -1,5 +1,6 @@
|
||||
import "server-only";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { cacheLife, cacheTag } from "next/cache";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import * as schema from "@/lib/db/schema";
|
||||
@@ -11,51 +12,54 @@ export type CommentWithUser = typeof schema.comment.$inferSelect & {
|
||||
user: CommentUser;
|
||||
};
|
||||
|
||||
/** Cache tag for a single page's comments; expired by the actions in lib/server/comments.ts */
|
||||
export const commentsTag = (pageSlug: string) => `comments-${pageSlug}`;
|
||||
|
||||
/**
|
||||
* Shared across server instances since it's read at request time (inside dynamic holes), not at prerender. Throws on
|
||||
* failure so an error is never cached.
|
||||
*/
|
||||
const getCachedComments = async (pageSlug: string): Promise<CommentWithUser[]> => {
|
||||
"use cache: remote";
|
||||
cacheLife("hours");
|
||||
cacheTag(commentsTag(pageSlug));
|
||||
|
||||
// Fetch all comments for the page with user details
|
||||
const commentsWithUsers = await db
|
||||
.select()
|
||||
.from(schema.comment)
|
||||
.innerJoin(schema.user, eq(schema.comment.userId, schema.user.id))
|
||||
.where(eq(schema.comment.pageSlug, pageSlug))
|
||||
.orderBy(desc(schema.comment.createdAt));
|
||||
|
||||
return commentsWithUsers.map(({ comment, user }) =>
|
||||
Object.assign(comment, {
|
||||
user: {
|
||||
// we're namely worried about keeping the user's email private here, but nothing sensitive is stored in the db
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all comments for a given slug, newest first, with each author's public profile fields
|
||||
*/
|
||||
export const getComments = async (pageSlug: string): Promise<CommentWithUser[]> => {
|
||||
try {
|
||||
// Fetch all comments for the page with user details
|
||||
const commentsWithUsers = await db
|
||||
.select()
|
||||
.from(schema.comment)
|
||||
.innerJoin(schema.user, eq(schema.comment.userId, schema.user.id))
|
||||
.where(eq(schema.comment.pageSlug, pageSlug))
|
||||
.orderBy(desc(schema.comment.createdAt));
|
||||
|
||||
return commentsWithUsers.map(({ comment, user }) =>
|
||||
Object.assign(comment, {
|
||||
user: {
|
||||
// we're namely worried about keeping the user's email private here, but nothing sensitive is stored in the db
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return await getCachedComments(pageSlug);
|
||||
} catch (error) {
|
||||
console.error("[data/comments] error fetching comments:", error);
|
||||
// Return empty array instead of throwing during prerendering
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the number of comments for a given slug (uncached -- call from inside a <Suspense> boundary)
|
||||
* Retrieves the number of comments for a given slug (shares getComments' cache entry)
|
||||
*/
|
||||
export const getCommentCount = async (slug: string): Promise<number> => {
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
count: sql<number>`cast(count(${schema.comment.id}) as int)`,
|
||||
})
|
||||
.from(schema.comment)
|
||||
.where(eq(schema.comment.pageSlug, slug));
|
||||
|
||||
return result[0]?.count ?? 0;
|
||||
} catch (error) {
|
||||
console.error("[data/comments] error fetching comment count:", error);
|
||||
return 0;
|
||||
}
|
||||
export const getCommentCount = async (pageSlug: string): Promise<number> => {
|
||||
const comments = await getComments(pageSlug);
|
||||
return comments.length;
|
||||
};
|
||||
+3
-3
@@ -12,12 +12,12 @@ export type PostStatsData = {
|
||||
|
||||
/**
|
||||
* View and comment counts for ALL pages, keyed by slug (e.g. "notes/dark-mode").
|
||||
* Cached briefly; invalidated by `revalidateTag("views")` / `revalidateTag("comments")`.
|
||||
* Cached briefly (views can lag by a minute or so); comment changes expire it via `updateTag("comments")`.
|
||||
*/
|
||||
export const getPostStats = async (): Promise<PostStatsData> => {
|
||||
"use cache";
|
||||
"use cache: remote";
|
||||
cacheLife("minutes");
|
||||
cacheTag("views", "comments");
|
||||
cacheTag("comments");
|
||||
|
||||
try {
|
||||
const [pageRows, commentRows] = await Promise.all([
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import "server-only";
|
||||
import { db } from "@/lib/db";
|
||||
import { page } from "@/lib/db/schema";
|
||||
|
||||
/**
|
||||
* Retrieves the numbers of views for ALL slugs (uncached)
|
||||
*/
|
||||
export const getAllViewCounts = async (): Promise<Record<string, number>> => {
|
||||
try {
|
||||
const pages = await db.select().from(page);
|
||||
return pages.reduce(
|
||||
(acc, p) => {
|
||||
acc[p.slug] = p.views;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[data/views] fatal error:", error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
+13
-9
@@ -1,11 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath, revalidateTag } from "next/cache";
|
||||
import { refresh, updateTag } from "next/cache";
|
||||
import { headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { commentsTag } from "@/lib/data/comments";
|
||||
import { db } from "@/lib/db";
|
||||
import * as schema from "@/lib/db/schema";
|
||||
import { getPostByPageSlug } from "@/lib/posts";
|
||||
@@ -66,9 +67,10 @@ export const createComment = async (data: {
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
// Revalidate page and the cached comment counts
|
||||
revalidatePath(`/${pageSlug}`);
|
||||
revalidateTag("comments", "max");
|
||||
// Expire this page's cached comments and the post stats, then refresh the commenter's view
|
||||
updateTag(commentsTag(pageSlug));
|
||||
updateTag("comments");
|
||||
refresh();
|
||||
} catch (error) {
|
||||
console.error("[server/comments] error creating comment:", error);
|
||||
throw new Error("Failed to create comment", { cause: error });
|
||||
@@ -119,8 +121,9 @@ export const updateComment = async (commentId: string, content: string) => {
|
||||
})
|
||||
.where(eq(schema.comment.id, commentId));
|
||||
|
||||
// Revalidate page
|
||||
revalidatePath(`/${comment.pageSlug}`);
|
||||
// Expire this page's cached comments, then refresh the commenter's view
|
||||
updateTag(commentsTag(comment.pageSlug));
|
||||
refresh();
|
||||
} catch (error) {
|
||||
console.error("[server/comments] error updating comment:", error);
|
||||
throw new Error("Failed to update comment", { cause: error });
|
||||
@@ -163,9 +166,10 @@ export const deleteComment = async (commentId: string) => {
|
||||
// Delete the comment
|
||||
await db.delete(schema.comment).where(eq(schema.comment.id, commentId));
|
||||
|
||||
// Revalidate page and the cached comment counts
|
||||
revalidatePath(`/${comment.pageSlug}`);
|
||||
revalidateTag("comments", "max");
|
||||
// Expire this page's cached comments and the post stats, then refresh the commenter's view
|
||||
updateTag(commentsTag(comment.pageSlug));
|
||||
updateTag("comments");
|
||||
refresh();
|
||||
} catch (error) {
|
||||
console.error("[server/comments] error deleting comment:", error);
|
||||
throw new Error("Failed to delete comment", { cause: error });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { page } from "@/lib/db/schema";
|
||||
@@ -27,9 +26,6 @@ export const incrementViews = async (slug: string): Promise<number> => {
|
||||
})
|
||||
.returning({ views: page.views });
|
||||
|
||||
// Mark the cached post stats (lib/data/stats.ts) stale so lists pick up the new count
|
||||
revalidateTag("views", "max");
|
||||
|
||||
return result.views;
|
||||
} catch (error) {
|
||||
console.error("[server/views] error incrementing views:", error);
|
||||
|
||||
Reference in new issue
Block a user