mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
4.5 KiB
4.5 KiB
description
| description |
|---|
| Convex patterns and conventions |
Convex Patterns Reference
Convex patterns and conventions for this markdown publishing framework.
Function structure
Every Convex function needs argument and return validators:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myQuery = query({
args: { slug: v.string() },
returns: v.union(v.object({...}), v.null()),
handler: async (ctx, args) => {
// implementation
},
});
If a function returns nothing, use returns: v.null() and return null;.
Always use indexes
Never use .filter() on queries. Define indexes in schema and use .withIndex():
// Good
const post = await ctx.db
.query("posts")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
// Bad - causes table scans
const post = await ctx.db
.query("posts")
.filter((q) => q.eq(q.field("slug"), args.slug))
.first();
Make mutations idempotent
Mutations should be safe to call multiple times:
export const heartbeat = mutation({
args: { sessionId: v.string(), currentPath: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const now = Date.now();
const existing = await ctx.db
.query("activeSessions")
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
.first();
if (existing) {
// Early return if recently updated with same data
if (existing.currentPath === args.currentPath &&
now - existing.lastSeen < 10000) {
return null;
}
await ctx.db.patch(existing._id, {
currentPath: args.currentPath,
lastSeen: now
});
return null;
}
await ctx.db.insert("activeSessions", { ...args, lastSeen: now });
return null;
},
});
Patch directly without reading
When you only need to update fields, patch directly:
// Good - patch directly
await ctx.db.patch(args.id, { content: args.content });
// Bad - unnecessary read creates conflict window
const doc = await ctx.db.get(args.id);
if (!doc) throw new Error("Not found");
await ctx.db.patch(args.id, { content: args.content });
Use event records for counters
Never increment counters on documents. Use separate event records:
// Good - insert event record
await ctx.db.insert("pageViews", {
path,
sessionId,
timestamp: Date.now()
});
// Bad - counter updates cause write conflicts
await ctx.db.patch(pageId, { views: page.views + 1 });
Schema indexes in this project
Key indexes defined in convex/schema.ts:
posts: defineTable({...})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
.searchIndex("search_title", { searchField: "title" })
.searchIndex("search_content", { searchField: "content" })
pages: defineTable({...})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
Common query patterns
Get post by slug
const post = await ctx.db
.query("posts")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
Get all published posts
const posts = await ctx.db
.query("posts")
.withIndex("by_published", (q) => q.eq("published", true))
.order("desc")
.collect();
Get featured items
const featured = await ctx.db
.query("posts")
.withIndex("by_featured", (q) => q.eq("featured", true))
.collect();
Full text search
const results = await ctx.db
.query("posts")
.withSearchIndex("search_content", (q) => q.search("content", searchTerm))
.take(10);
File locations
| File | Purpose |
|---|---|
convex/schema.ts |
Database schema and indexes |
convex/posts.ts |
Post queries/mutations |
convex/pages.ts |
Page queries/mutations |
convex/stats.ts |
Analytics (heartbeat, pageViews) |
convex/search.ts |
Full text search |
convex/http.ts |
HTTP endpoints |
convex/rss.ts |
RSS feed generation |
convex/crons.ts |
Scheduled jobs |
Write conflict prevention
This project uses specific patterns to avoid write conflicts:
Backend (convex/stats.ts):
- 10-second dedup window for heartbeats
- Early return when session was recently updated
- Indexed queries for efficient lookups
Frontend (src/hooks/usePageTracking.ts):
- 5-second debounce window using refs
- Pending state tracking prevents overlapping calls
- Path tracking skips redundant heartbeats