Skip to content

Commit 1cf0d0d

Browse files
committed
feat(performance): fix inconsistent 404 lookups, improve cache and post loading times
1 parent 98511b0 commit 1cf0d0d

File tree

8 files changed

+139
-52
lines changed

8 files changed

+139
-52
lines changed

src/server/api/blog/create.post.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export default defineEventHandler(async (event) => {
7272

7373
// Invalidate caches
7474
await kv.del('nuxtpress:blog_posts_list');
75+
await kv.del('nuxtpress:blog_posts_list:v1');
7576

7677
return {
7778
id,

src/server/api/blog/find.get.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { eq } from 'drizzle-orm';
1+
import { and, desc, eq, gte, lt } from 'drizzle-orm';
22
import { db } from 'hub:db';
33
import { kv } from 'hub:kv';
44
import { blogPosts } from '~/server/db/schema';
@@ -32,44 +32,74 @@ export default defineEventHandler(async (event) => {
3232
});
3333
}
3434

35-
const cacheKey = `nuxtpress:blog_post:${slug}:${year}:${month}:${day}`;
35+
const year0 = Number(year);
36+
const month0 = Number(month);
37+
const day0 = Number(day);
38+
39+
if (
40+
!Number.isInteger(year0) ||
41+
!Number.isInteger(month0) ||
42+
!Number.isInteger(day0) ||
43+
year0 < 2000 ||
44+
year0 > new Date().getUTCFullYear() + 1 ||
45+
month0 < 1 ||
46+
month0 > 12 ||
47+
day0 < 1 ||
48+
day0 > 31
49+
) {
50+
throw createError({
51+
statusCode: 400,
52+
statusMessage: 'Date is out of range'
53+
});
54+
}
55+
56+
const cacheKey = `nuxtpress:blog_post:v2:${slug}:${year0}:${month0}:${day0}`;
3657

3758
// Check cache first
3859
const cached = await kv.get(cacheKey);
3960
if (cached && typeof cached === 'string') {
40-
return JSON.parse(cached);
61+
try {
62+
return JSON.parse(cached);
63+
} catch {
64+
await kv.del(cacheKey);
65+
}
4166
}
4267

43-
// Get all posts with the same slug
44-
const rows = await db.select().from(blogPosts).where(eq(blogPosts.slug, slug));
68+
const startOfDayUtcMs = Date.UTC(year0, month0 - 1, day0, 0, 0, 0, 0);
69+
const endOfDayUtcMs = startOfDayUtcMs + 24 * 60 * 60 * 1000;
4570

46-
const posts = rows.map((row) => ({
47-
...row,
48-
created_at: new Date(row.createdAt),
49-
updated_at: new Date(row.updatedAt),
50-
thumbnail: row.thumbnail
51-
? Uint8Array.from(atob(row.thumbnail), (c) => c.charCodeAt(0))
52-
: undefined,
53-
thumbnail_url: row.thumbnailUrl,
54-
tags: row.tags ? row.tags.split(',').map((t: string) => t.trim()) : []
55-
})) as BlogPost[];
71+
const rows = await db
72+
.select()
73+
.from(blogPosts)
74+
.where(
75+
and(
76+
eq(blogPosts.slug, slug),
77+
gte(blogPosts.createdAt, new Date(startOfDayUtcMs)),
78+
lt(blogPosts.createdAt, new Date(endOfDayUtcMs))
79+
)
80+
)
81+
.orderBy(desc(blogPosts.createdAt))
82+
.limit(1);
5683

57-
if (!posts || posts.length === 0) {
84+
const row = rows[0];
85+
86+
if (!row) {
5887
throw createError({
5988
statusCode: 404,
6089
statusMessage: 'Post not found'
6190
});
6291
}
6392

64-
// Construct the target date from the URL params
65-
const targetDate = new Date(Number(year), Number(month) - 1, Number(day));
66-
67-
// Find the post with the closest date to the target
68-
const res = posts.reduce((closest, current) => {
69-
const closestDiff = Math.abs(closest.created_at.getTime() - targetDate.getTime());
70-
const currentDiff = Math.abs(current.created_at.getTime() - targetDate.getTime());
71-
return currentDiff < closestDiff ? current : closest;
72-
});
93+
const res = {
94+
...row,
95+
created_at: new Date(row.createdAt),
96+
updated_at: new Date(row.updatedAt),
97+
thumbnail: row.thumbnail
98+
? Uint8Array.from(atob(row.thumbnail), (c) => c.charCodeAt(0))
99+
: undefined,
100+
thumbnail_url: row.thumbnailUrl || undefined,
101+
tags: row.tags ? row.tags.split(',').map((t: string) => t.trim()) : []
102+
} as BlogPost;
73103

74104
// Cache the result
75105
await kv.set(cacheKey, JSON.stringify(res), { ttl: 60 * 60 * 4 }); // 4 hours

src/server/api/blog/list.get.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import { desc } from 'drizzle-orm';
22
import { db } from 'hub:db';
3+
import { kv } from 'hub:kv';
34
import { blogPosts } from '~/server/db/schema';
45
import { ensureDatabase } from '~/server/utils/db';
56
import { BlogPost } from '~/shared/types';
67

78
export default defineEventHandler(async (_) => {
89
await ensureDatabase();
9-
const rows = await db.select().from(blogPosts).orderBy(desc(blogPosts.createdAt));
10+
const cacheKey = 'nuxtpress:blog_posts_list:v1';
11+
12+
const cached = await kv.get(cacheKey);
13+
if (cached && typeof cached === 'string') {
14+
try {
15+
return JSON.parse(cached) as BlogPost[];
16+
} catch {
17+
await kv.del(cacheKey);
18+
}
19+
}
1020

11-
return rows.map(
21+
const rows = await db.select().from(blogPosts).orderBy(desc(blogPosts.createdAt));
22+
const posts = rows.map(
1223
(row) =>
1324
({
1425
...row,
@@ -21,4 +32,8 @@ export default defineEventHandler(async (_) => {
2132
tags: row.tags ? row.tags.split(',').map((t) => t.trim()) : []
2233
}) satisfies BlogPost
2334
) as BlogPost[];
35+
36+
await kv.set(cacheKey, JSON.stringify(posts), { ttl: 60 * 3 }); // 3 minutes
37+
38+
return posts;
2439
});

src/server/api/blog/remove.delete.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ export default defineEventHandler(async (event) => {
2929

3030
// Invalidate caches
3131
await kv.del('nuxtpress:blog_posts_list');
32+
await kv.del('nuxtpress:blog_posts_list:v1');
3233

3334
if (post?.slug) {
3435
await kv.del(`nuxtpress:slug_exists:${post.slug}`);
3536

3637
const postDate = new Date(post.createdAt);
3738
const cacheKey = `nuxtpress:blog_post:${post.slug}:${postDate.getUTCFullYear()}:${postDate.getUTCMonth() + 1}:${postDate.getUTCDate()}`;
39+
const cacheKeyV2 = `nuxtpress:blog_post:v2:${post.slug}:${postDate.getUTCFullYear()}:${postDate.getUTCMonth() + 1}:${postDate.getUTCDate()}`;
3840
await kv.del(cacheKey);
41+
await kv.del(cacheKeyV2);
3942
}
4043
});

src/server/api/blog/update.patch.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,23 @@ export default defineEventHandler(async (event) => {
6262

6363
// invalidate caches
6464
await kv.del('nuxtpress:blog_posts_list');
65+
await kv.del('nuxtpress:blog_posts_list:v1');
6566
await kv.del(`nuxtpress:slug_exists:${post.slug}`);
6667

6768
if (oldPost) {
6869
const oldDate = new Date(oldPost.createdAt);
6970
const oldCacheKey = `nuxtpress:blog_post:${oldPost.slug}:${oldDate.getUTCFullYear()}:${oldDate.getUTCMonth() + 1}:${oldDate.getUTCDate()}`;
71+
const oldCacheKeyV2 = `nuxtpress:blog_post:v2:${oldPost.slug}:${oldDate.getUTCFullYear()}:${oldDate.getUTCMonth() + 1}:${oldDate.getUTCDate()}`;
7072
await kv.del(oldCacheKey);
73+
await kv.del(oldCacheKeyV2);
7174

7275
if (oldPost.slug !== post.slug) {
7376
await kv.del(`nuxtpress:slug_exists:${oldPost.slug}`);
7477
}
7578

7679
const newCacheKey = `nuxtpress:blog_post:${post.slug}:${oldDate.getUTCFullYear()}:${oldDate.getUTCMonth() + 1}:${oldDate.getUTCDate()}`;
80+
const newCacheKeyV2 = `nuxtpress:blog_post:v2:${post.slug}:${oldDate.getUTCFullYear()}:${oldDate.getUTCMonth() + 1}:${oldDate.getUTCDate()}`;
7781
await kv.del(newCacheKey);
82+
await kv.del(newCacheKeyV2);
7883
}
7984
});

src/server/db/schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export const blogPosts = sqliteTable(
2020
},
2121
(table) => [
2222
index('idx_blog_posts_slug').on(table.slug),
23-
index('idx_blog_posts_created_at').on(table.createdAt)
23+
index('idx_blog_posts_created_at').on(table.createdAt),
24+
index('idx_blog_posts_slug_created_at').on(table.slug, table.createdAt)
2425
]
2526
);
2627

src/server/plugins/db-init.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
import { ensureDatabase } from '~/server/utils/db';
2-
3-
export default defineNitroPlugin(async (nitroApp) => {
4-
if (nitroApp.hooks) {
5-
nitroApp.hooks.hook('request', async () => {
6-
await ensureDatabase();
7-
});
8-
}
1+
export default defineNitroPlugin(() => {
2+
// Keep DB initialization in request handlers to avoid global-scope I/O in Cloudflare workers.
93
});

src/server/utils/db.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ import { kv } from 'hub:kv';
55
let isInitialized = false;
66
let initPromise: Promise<void> | null = null;
77

8-
const CURRENT_MIGRATION_VERSION = 1;
8+
const CURRENT_MIGRATION_VERSION = 2;
9+
const MIGRATION_VERSION_KEY = 'nuxtpress:db_migration_version';
10+
11+
async function hasBlogPostsTable() {
12+
try {
13+
await db.run(sql`SELECT 1 FROM blog_posts LIMIT 1`);
14+
return true;
15+
} catch {
16+
return false;
17+
}
18+
}
919

1020
/**
1121
* Ensures the database table exists and is migrated.
@@ -14,16 +24,29 @@ const CURRENT_MIGRATION_VERSION = 1;
1424
export async function ensureDatabase() {
1525
if (isInitialized) return;
1626

17-
const cachedVersion = await kv.get<number>('nuxtpress:db_migration_version');
18-
if (cachedVersion === CURRENT_MIGRATION_VERSION) {
19-
isInitialized = true;
20-
return;
21-
}
22-
2327
if (initPromise) return initPromise;
2428

2529
initPromise = (async () => {
2630
try {
31+
let cachedVersion: number | null = null;
32+
try {
33+
cachedVersion = await kv.get<number>(MIGRATION_VERSION_KEY);
34+
} catch (error) {
35+
console.warn('Unable to read migration version cache:', error);
36+
}
37+
38+
if (cachedVersion === CURRENT_MIGRATION_VERSION) {
39+
const tableExists = await hasBlogPostsTable();
40+
if (tableExists) {
41+
isInitialized = true;
42+
return;
43+
}
44+
45+
console.warn(
46+
'Migration cache version matched but blog_posts was unavailable. Re-running migrations.'
47+
);
48+
}
49+
2750
console.log('📦 Running database migrations...');
2851

2952
await db.run(sql`
@@ -44,6 +67,9 @@ export async function ensureDatabase() {
4467
await db.run(
4568
sql`CREATE INDEX IF NOT EXISTS idx_blog_posts_created_at ON blog_posts(created_at)`
4669
);
70+
await db.run(
71+
sql`CREATE INDEX IF NOT EXISTS idx_blog_posts_slug_created_at ON blog_posts(slug, created_at)`
72+
);
4773

4874
// Migration: Convert ISO string timestamps to integer milliseconds
4975
const isoFormatCheck = await db.run(sql`
@@ -55,7 +81,7 @@ export async function ensureDatabase() {
5581
if (
5682
isoFormatCheck.rows &&
5783
isoFormatCheck.rows.length > 0 &&
58-
(isoFormatCheck.rows[0] as any).count > 0
84+
Number((isoFormatCheck.rows[0] as any).count) > 0
5985
) {
6086
console.log('📝 Converting ISO timestamp strings to integers...');
6187

@@ -98,7 +124,7 @@ export async function ensureDatabase() {
98124
if (
99125
oldFormatCheck.rows &&
100126
oldFormatCheck.rows.length > 0 &&
101-
(oldFormatCheck.rows[0] as any).count > 0
127+
Number((oldFormatCheck.rows[0] as any).count) > 0
102128
) {
103129
console.log('📝 Converting second timestamps to milliseconds...');
104130
await db.run(sql`
@@ -112,16 +138,28 @@ export async function ensureDatabase() {
112138
}
113139

114140
console.log('✓ Database migrations completed');
141+
142+
const tableExists = await hasBlogPostsTable();
143+
if (!tableExists) {
144+
throw new Error('Database initialization verification failed for blog_posts');
145+
}
146+
147+
isInitialized = true;
148+
149+
try {
150+
await kv.set(MIGRATION_VERSION_KEY, CURRENT_MIGRATION_VERSION);
151+
} catch (error) {
152+
console.warn('Unable to cache migration version in KV:', error);
153+
}
115154
} catch (error: any) {
116155
console.error('Database migration error:', error);
117-
// Don't throw - let the app continue, it might be a permission issue
118-
// The migrations might have already run
119-
} finally {
120-
isInitialized = true;
121-
await kv.set('nuxtpress:db_migration_version', CURRENT_MIGRATION_VERSION);
156+
throw error;
122157
}
123158
})();
124159

125-
await initPromise;
126-
initPromise = null;
160+
try {
161+
await initPromise;
162+
} finally {
163+
initPromise = null;
164+
}
127165
}

0 commit comments

Comments
 (0)