convex-anti-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex Anti-Patterns & Agent Rules
Convex反模式与Agent规则
Overview
概述
This skill documents critical mistakes to avoid in Convex development and rules that agents must follow. Every pattern here has caused real production issues.
本技能记录了Convex开发中需避免的严重错误,以及Agent必须遵循的规则。这里提到的每一种模式都曾引发过实际生产环境中的问题。
TypeScript: NEVER Use any
Type
anyTypeScript:绝对禁止使用any
类型
anyCRITICAL RULE: This codebase has enabled. Using will cause build failures.
@typescript-eslint/no-explicit-anyany❌ WRONG:
typescript
function handleData(data: any) { ... }
const items: any[] = [];
args: { data: v.any() } // Also avoid!✅ CORRECT:
typescript
function handleData(data: Doc<"items">) { ... }
const items: Doc<"items">[] = [];
args: { data: v.object({ field: v.string() }) }关键规则: 本代码库已启用规则。使用类型会导致构建失败。
@typescript-eslint/no-explicit-anyany❌ 错误示例:
typescript
function handleData(data: any) { ... }
const items: any[] = [];
args: { data: v.any() } // 同样要避免!✅ 正确示例:
typescript
function handleData(data: Doc<"items">) { ... }
const items: Doc<"items">[] = [];
args: { data: v.object({ field: v.string() }) }When to Use This Skill
何时使用本技能
Use this skill when:
- Reviewing Convex code for issues
- Debugging mysterious errors
- Understanding why code doesn't work as expected
- Learning Convex best practices by counter-example
- Checking code against known anti-patterns
在以下场景中使用本技能:
- 评审Convex代码以排查问题
- 调试难以定位的错误
- 理解代码未按预期运行的原因
- 通过反例学习Convex最佳实践
- 对照已知反模式检查代码
Critical Anti-Patterns
严重反模式
Anti-Pattern 1: fetch() in Mutations
反模式1:在Mutation中调用fetch()
Mutations must be deterministic. External calls break this guarantee.
❌ WRONG:
typescript
export const createOrder = mutation({
args: { productId: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ Mutations cannot make external HTTP calls!
const price = await fetch(
`https://api.stripe.com/prices/${args.productId}`
);
await ctx.db.insert("orders", {
productId: args.productId,
price: await price.json(),
});
return null;
},
});✅ CORRECT:
typescript
// Mutation creates record, schedules action for external call
export const createOrder = mutation({
args: { productId: v.string() },
returns: v.id("orders"),
handler: async (ctx, args) => {
const orderId = await ctx.db.insert("orders", {
productId: args.productId,
status: "pending",
});
await ctx.scheduler.runAfter(0, internal.orders.fetchPrice, { orderId });
return orderId;
},
});
// Action handles external API call
export const fetchPrice = internalAction({
args: { orderId: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => {
const order = await ctx.runQuery(internal.orders.getById, {
orderId: args.orderId,
});
if (!order) return null;
const response = await fetch(
`https://api.stripe.com/prices/${order.productId}`
);
const priceData = await response.json();
await ctx.runMutation(internal.orders.updatePrice, {
orderId: args.orderId,
price: priceData.unit_amount,
});
return null;
},
});Mutation必须是确定性的。外部调用会打破这一保证。
❌ 错误示例:
typescript
export const createOrder = mutation({
args: { productId: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ Mutation中不能发起外部HTTP调用!
const price = await fetch(
`https://api.stripe.com/prices/${args.productId}`
);
await ctx.db.insert("orders", {
productId: args.productId,
price: await price.json(),
});
return null;
},
});✅ 正确示例:
typescript
// Mutation创建记录,调度Action处理外部调用
export const createOrder = mutation({
args: { productId: v.string() },
returns: v.id("orders"),
handler: async (ctx, args) => {
const orderId = await ctx.db.insert("orders", {
productId: args.productId,
status: "pending",
});
await ctx.scheduler.runAfter(0, internal.orders.fetchPrice, { orderId });
return orderId;
},
});
// Action处理外部API调用
export const fetchPrice = internalAction({
args: { orderId: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => {
const order = await ctx.runQuery(internal.orders.getById, {
orderId: args.orderId,
});
if (!order) return null;
const response = await fetch(
`https://api.stripe.com/prices/${order.productId}`
);
const priceData = await response.json();
await ctx.runMutation(internal.orders.updatePrice, {
orderId: args.orderId,
price: priceData.unit_amount,
});
return null;
},
});Anti-Pattern 2: ctx.db in Actions
反模式2:在Action中使用ctx.db
Actions don't have database access. This is a common source of TypeScript errors.
❌ WRONG:
typescript
export const processData = action({
args: { id: v.id("items") },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ Actions don't have ctx.db!
const item = await ctx.db.get(args.id); // TypeScript Error!
return null;
},
});✅ CORRECT:
typescript
export const processData = action({
args: { id: v.id("items") },
returns: v.null(),
handler: async (ctx, args) => {
// ✅ Use ctx.runQuery to read
const item = await ctx.runQuery(internal.items.getById, { id: args.id });
// Process with external APIs...
const result = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(item),
});
// ✅ Use ctx.runMutation to write
await ctx.runMutation(internal.items.updateResult, {
id: args.id,
result: await result.json(),
});
return null;
},
});Action没有数据库访问权限。这是TypeScript错误的常见来源。
❌ 错误示例:
typescript
export const processData = action({
args: { id: v.id("items") },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ Action中没有ctx.db!
const item = await ctx.db.get(args.id); // TypeScript错误!
return null;
},
});✅ 正确示例:
typescript
export const processData = action({
args: { id: v.id("items") },
returns: v.null(),
handler: async (ctx, args) => {
// ✅ 使用ctx.runQuery读取数据
const item = await ctx.runQuery(internal.items.getById, { id: args.id });
// 调用外部API处理数据...
const result = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(item),
});
// ✅ 使用ctx.runMutation写入数据
await ctx.runMutation(internal.items.updateResult, {
id: args.id,
result: await result.json(),
});
return null;
},
});Anti-Pattern 3: Missing returns Validator
反模式3:缺少returns验证器
Every function must have an explicit validator.
returns❌ WRONG:
typescript
export const doSomething = mutation({
args: { data: v.string() },
// ❌ Missing returns!
handler: async (ctx, args) => {
await ctx.db.insert("items", { data: args.data });
// Implicitly returns undefined
},
});✅ CORRECT:
typescript
export const doSomething = mutation({
args: { data: v.string() },
returns: v.null(), // ✅ Explicit returns validator
handler: async (ctx, args) => {
await ctx.db.insert("items", { data: args.data });
return null; // ✅ Explicit return value
},
});每个函数都必须有显式的验证器。
returns❌ 错误示例:
typescript
export const doSomething = mutation({
args: { data: v.string() },
// ❌ 缺少returns!
handler: async (ctx, args) => {
await ctx.db.insert("items", { data: args.data });
// 隐式返回undefined
},
});✅ 正确示例:
typescript
export const doSomething = mutation({
args: { data: v.string() },
returns: v.null(), // ✅ 显式的returns验证器
handler: async (ctx, args) => {
await ctx.db.insert("items", { data: args.data });
return null; // ✅ 显式返回值
},
});Anti-Pattern 4: Using .filter() on Queries
反模式4:在Query中使用.filter()
.filter()❌ WRONG:
typescript
export const getActiveUsers = query({
args: {},
returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
handler: async (ctx) => {
// ❌ Full table scan!
return await ctx.db
.query("users")
.filter((q) => q.eq(q.field("status"), "active"))
.collect();
},
});✅ CORRECT:
typescript
// Schema: .index("by_status", ["status"])
export const getActiveUsers = query({
args: {},
returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
handler: async (ctx) => {
// ✅ Uses index
return await ctx.db
.query("users")
.withIndex("by_status", (q) => q.eq("status", "active"))
.collect();
},
});.filter()❌ 错误示例:
typescript
export const getActiveUsers = query({
args: {},
returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
handler: async (ctx) => {
// ❌ 全表扫描!
return await ctx.db
.query("users")
.filter((q) => q.eq(q.field("status"), "active"))
.collect();
},
});✅ 正确示例:
typescript
// 数据库Schema:.index("by_status", ["status"])
export const getActiveUsers = query({
args: {},
returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
handler: async (ctx) => {
// ✅ 使用索引
return await ctx.db
.query("users")
.withIndex("by_status", (q) => q.eq("status", "active"))
.collect();
},
});Anti-Pattern 5: Unbounded .collect()
反模式5:无限制的.collect()
Never collect without limits on potentially large tables.
❌ WRONG:
typescript
export const getAllMessages = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({ content: v.string() })),
handler: async (ctx, args) => {
// ❌ Could return millions of records!
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
},
});✅ CORRECT:
typescript
export const getRecentMessages = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({ content: v.string() })),
handler: async (ctx, args) => {
// ✅ Bounded with take()
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(50);
},
});对于可能很大的表,绝对不要无限制地使用.collect()。
❌ 错误示例:
typescript
export const getAllMessages = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({ content: v.string() })),
handler: async (ctx, args) => {
// ❌ 可能返回数百万条记录!
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
},
});✅ 正确示例:
typescript
export const getRecentMessages = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({ content: v.string() })),
handler: async (ctx, args) => {
// ✅ 使用take()限制数量
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(50);
},
});Anti-Pattern 6: .collect().length for Counts
反模式6:使用.collect().length统计数量
Collecting just to count is wasteful.
❌ WRONG:
typescript
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.number(),
handler: async (ctx, args) => {
// ❌ Loads all messages just to count!
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
return messages.length;
},
});✅ CORRECT:
typescript
// Option 1: Bounded count with "99+" display
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.string(),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.take(100);
return messages.length === 100 ? "99+" : String(messages.length);
},
});
// Option 2: Denormalized counter (best for high traffic)
// Maintain messageCount field in channels table
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.number(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
return channel?.messageCount ?? 0;
},
});仅为了统计数量而调用.collect()是低效的。
❌ 错误示例:
typescript
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.number(),
handler: async (ctx, args) => {
// ❌ 加载所有消息只是为了统计数量!
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
return messages.length;
},
});✅ 正确示例:
typescript
// 选项1:带限制的统计,显示“99+”
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.string(),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.take(100);
return messages.length === 100 ? "99+" : String(messages.length);
},
});
// 选项2:反规范化计数器(高流量场景最佳方案)
// 在channels表中维护messageCount字段
export const getMessageCount = query({
args: { channelId: v.id("channels") },
returns: v.number(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
return channel?.messageCount ?? 0;
},
});Anti-Pattern 7: N+1 Query Pattern
反模式7:N+1查询模式
Loading related documents one by one.
❌ WRONG:
typescript
export const getPostsWithAuthors = query({
args: {},
returns: v.array(
v.object({
post: v.object({ title: v.string() }),
author: v.object({ name: v.string() }),
})
),
handler: async (ctx) => {
const posts = await ctx.db.query("posts").take(10);
// ❌ N additional queries!
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
post: { title: post.title },
author: await ctx.db
.get(post.authorId)
.then((a) => ({ name: a!.name })),
}))
);
return postsWithAuthors;
},
});✅ CORRECT:
typescript
import { getAll } from "convex-helpers/server/relationships";
export const getPostsWithAuthors = query({
args: {},
returns: v.array(
v.object({
post: v.object({ title: v.string() }),
author: v.union(v.object({ name: v.string() }), v.null()),
})
),
handler: async (ctx) => {
const posts = await ctx.db.query("posts").take(10);
// ✅ Batch fetch all authors
const authorIds = [...new Set(posts.map((p) => p.authorId))];
const authors = await getAll(ctx.db, authorIds);
const authorMap = new Map(
authors
.filter((a): a is NonNullable<typeof a> => a !== null)
.map((a) => [a._id, a])
);
return posts.map((post) => ({
post: { title: post.title },
author: authorMap.get(post.authorId)
? { name: authorMap.get(post.authorId)!.name }
: null,
}));
},
});逐个加载关联文档。
❌ 错误示例:
typescript
export const getPostsWithAuthors = query({
args: {},
returns: v.array(
v.object({
post: v.object({ title: v.string() }),
author: v.object({ name: v.string() }),
})
),
handler: async (ctx) => {
const posts = await ctx.db.query("posts").take(10);
// ❌ 额外发起N次查询!
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
post: { title: post.title },
author: await ctx.db
.get(post.authorId)
.then((a) => ({ name: a!.name })),
}))
);
return postsWithAuthors;
},
});✅ 正确示例:
typescript
import { getAll } from "convex-helpers/server/relationships";
export const getPostsWithAuthors = query({
args: {},
returns: v.array(
v.object({
post: v.object({ title: v.string() }),
author: v.union(v.object({ name: v.string() }), v.null()),
})
),
handler: async (ctx) => {
const posts = await ctx.db.query("posts").take(10);
// ✅ 批量获取所有作者
const authorIds = [...new Set(posts.map((p) => p.authorId))];
const authors = await getAll(ctx.db, authorIds);
const authorMap = new Map(
authors
.filter((a): a is NonNullable<typeof a> => a !== null)
.map((a) => [a._id, a])
);
return posts.map((post) => ({
post: { title: post.title },
author: authorMap.get(post.authorId)
? { name: authorMap.get(post.authorId)!.name }
: null,
}));
},
});Anti-Pattern 8: Global Counter (Hot Spot)
反模式8:全局计数器(热点)
Single document updates cause OCC conflicts under load.
❌ WRONG:
typescript
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
// ❌ Every request writes to same document!
const stats = await ctx.db.query("globalStats").unique();
await ctx.db.patch(stats!._id, { views: stats!.views + 1 });
return null;
},
});✅ CORRECT:
typescript
// Option 1: Sharding
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
// ✅ Write to random shard
const shardId = Math.floor(Math.random() * 10);
await ctx.db.insert("viewShards", { shardId, delta: 1 });
return null;
},
});
// Read by aggregating shards
export const getPageViews = query({
args: {},
returns: v.number(),
handler: async (ctx) => {
const shards = await ctx.db.query("viewShards").collect();
return shards.reduce((sum, s) => sum + s.delta, 0);
},
});
// Option 2: Use Workpool to serialize
import { Workpool } from "@convex-dev/workpool";
const counterPool = new Workpool(components.workpool, { maxParallelism: 1 });
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await counterPool.enqueueMutation(ctx, internal.stats.doIncrement, {});
return null;
},
});对单个文档的更新会在高负载下引发OCC冲突。
❌ 错误示例:
typescript
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
// ❌ 每个请求都写入同一个文档!
const stats = await ctx.db.query("globalStats").unique();
await ctx.db.patch(stats!._id, { views: stats!.views + 1 });
return null;
},
});✅ 正确示例:
typescript
// 选项1:分片处理
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
// ✅ 写入随机分片
const shardId = Math.floor(Math.random() * 10);
await ctx.db.insert("viewShards", { shardId, delta: 1 });
return null;
},
});
// 通过聚合分片数据读取统计值
export const getPageViews = query({
args: {},
returns: v.number(),
handler: async (ctx) => {
const shards = await ctx.db.query("viewShards").collect();
return shards.reduce((sum, s) => sum + s.delta, 0);
},
});
// 选项2:使用Workpool序列化请求
import { Workpool } from "@convex-dev/workpool";
const counterPool = new Workpool(components.workpool, { maxParallelism: 1 });
export const incrementPageViews = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await counterPool.enqueueMutation(ctx, internal.stats.doIncrement, {});
return null;
},
});Anti-Pattern 9: Using v.bigint() (Deprecated)
反模式9:使用v.bigint()(已废弃)
❌ WRONG:
typescript
export default defineSchema({
counters: defineTable({
value: v.bigint(), // ❌ Deprecated!
}),
});✅ CORRECT:
typescript
export default defineSchema({
counters: defineTable({
value: v.int64(), // ✅ Use v.int64()
}),
});❌ 错误示例:
typescript
export default defineSchema({
counters: defineTable({
value: v.bigint(), // ❌ 已废弃!
}),
});✅ 正确示例:
typescript
export default defineSchema({
counters: defineTable({
value: v.int64(), // ✅ 使用v.int64()
}),
});Anti-Pattern 10: Missing System Fields in Return Validators
反模式10:返回验证器中缺少系统字段
❌ WRONG:
typescript
export const getUser = query({
args: { userId: v.id("users") },
returns: v.object({
// ❌ Missing _id and _creationTime!
name: v.string(),
email: v.string(),
}),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId); // Returns full doc including system fields
},
});✅ CORRECT:
typescript
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(
v.object({
_id: v.id("users"), // ✅ Include system fields
_creationTime: v.number(),
name: v.string(),
email: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});❌ 错误示例:
typescript
export const getUser = query({
args: { userId: v.id("users") },
returns: v.object({
// ❌ 缺少_id和_creationTime!
name: v.string(),
email: v.string(),
}),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId); // 返回包含系统字段的完整文档
},
});✅ 正确示例:
typescript
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(
v.object({
_id: v.id("users"), // ✅ 包含系统字段
_creationTime: v.number(),
name: v.string(),
email: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});Anti-Pattern 11: Public Functions for Internal Logic
反模式11:将内部逻辑设为公共函数
❌ WRONG:
typescript
// ❌ This is callable by any client!
export const deleteUserData = mutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
// Dangerous operation exposed publicly
await ctx.db.delete(args.userId);
return null;
},
});✅ CORRECT:
typescript
// Internal mutation - not callable by clients
export const deleteUserData = internalMutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.userId);
return null;
},
});
// Public mutation with auth check
export const requestAccountDeletion = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// Schedule internal mutation
await ctx.scheduler.runAfter(0, internal.users.deleteUserData, {
userId: user._id,
});
return null;
},
});❌ 错误示例:
typescript
// ❌ 任何客户端都可以调用此函数!
export const deleteUserData = mutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
// 危险操作被公开暴露
await ctx.db.delete(args.userId);
return null;
},
});✅ 正确示例:
typescript
// 内部Mutation - 客户端无法直接调用
export const deleteUserData = internalMutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.userId);
return null;
},
});
// 带权限校验的公共Mutation
export const requestAccountDeletion = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("未授权");
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("用户不存在");
// 调度内部Mutation执行
await ctx.scheduler.runAfter(0, internal.users.deleteUserData, {
userId: user._id,
});
return null;
},
});Anti-Pattern 12: Non-Transactional Actions for Data Consistency
反模式12:使用非事务性Action保证数据一致性
❌ WRONG:
typescript
export const transferFunds = action({
args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ These are separate transactions - could leave inconsistent state!
await ctx.runMutation(internal.accounts.debit, {
accountId: args.from,
amount: args.amount,
});
// If this fails, money was debited but not credited!
await ctx.runMutation(internal.accounts.credit, {
accountId: args.to,
amount: args.amount,
});
return null;
},
});✅ CORRECT:
typescript
// Single atomic mutation
export const transferFunds = mutation({
args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// ✅ All in one transaction - all succeed or all fail
const fromAccount = await ctx.db.get(args.from);
const toAccount = await ctx.db.get(args.to);
if (!fromAccount || !toAccount) throw new Error("Account not found");
if (fromAccount.balance < args.amount)
throw new Error("Insufficient funds");
await ctx.db.patch(args.from, {
balance: fromAccount.balance - args.amount,
});
await ctx.db.patch(args.to, { balance: toAccount.balance + args.amount });
return null;
},
});❌ 错误示例:
typescript
export const transferFunds = action({
args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ 这些是独立的事务 - 可能导致数据状态不一致!
await ctx.runMutation(internal.accounts.debit, {
accountId: args.from,
amount: args.amount,
});
// 如果这一步失败,资金已被扣除但未转入目标账户!
await ctx.runMutation(internal.accounts.credit, {
accountId: args.to,
amount: args.amount,
});
return null;
},
});✅ 正确示例:
typescript
// 单个原子性Mutation
export const transferFunds = mutation({
args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// ✅ 所有操作在一个事务中 - 要么全部成功,要么全部失败
const fromAccount = await ctx.db.get(args.from);
const toAccount = await ctx.db.get(args.to);
if (!fromAccount || !toAccount) throw new Error("账户不存在");
if (fromAccount.balance < args.amount)
throw new Error("余额不足");
await ctx.db.patch(args.from, {
balance: fromAccount.balance - args.amount,
});
await ctx.db.patch(args.to, { balance: toAccount.balance + args.amount });
return null;
},
});Anti-Pattern 13: Redundant Indexes
反模式13:冗余索引
❌ WRONG:
typescript
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
.index("by_channel", ["channelId"]) // ❌ Redundant!
.index("by_channel_author", ["channelId", "authorId"]),
});✅ CORRECT:
typescript
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
// ✅ Single compound index serves both query patterns
.index("by_channel_author", ["channelId", "authorId"]),
});
// Use prefix matching for channel-only queries:
// .withIndex("by_channel_author", (q) => q.eq("channelId", id))❌ 错误示例:
typescript
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
.index("by_channel", ["channelId"]) // ❌ 冗余!
.index("by_channel_author", ["channelId", "authorId"]),
});✅ 正确示例:
typescript
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
// ✅ 单个复合索引可满足两种查询模式
.index("by_channel_author", ["channelId", "authorId"]),
});
// 针对仅按channel查询的场景使用前缀匹配:
// .withIndex("by_channel_author", (q) => q.eq("channelId", id))Anti-Pattern 14: Using v.string() for IDs
反模式14:使用v.string()表示ID
❌ WRONG:
typescript
export const getMessage = query({
args: { messageId: v.string() }, // ❌ Should be v.id()
returns: v.null(),
handler: async (ctx, args) => {
// Type error or runtime error
return await ctx.db.get(args.messageId as Id<"messages">);
},
});✅ CORRECT:
typescript
export const getMessage = query({
args: { messageId: v.id("messages") }, // ✅ Proper ID type
returns: v.union(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
content: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.messageId);
},
});❌ 错误示例:
typescript
export const getMessage = query({
args: { messageId: v.string() }, // ❌ 应该使用v.id()
returns: v.null(),
handler: async (ctx, args) => {
// 类型错误或运行时错误
return await ctx.db.get(args.messageId as Id<"messages">);
},
});✅ 正确示例:
typescript
export const getMessage = query({
args: { messageId: v.id("messages") }, // ✅ 正确的ID类型
returns: v.union(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
content: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.messageId);
},
});Anti-Pattern 15: Retry Without Backoff or Jitter
反模式15:重试时不使用退避算法或抖动
❌ WRONG:
typescript
export const processWithRetry = internalAction({
args: { jobId: v.id("jobs"), attempt: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
try {
// Process...
} catch (error) {
if (args.attempt < 5) {
// ❌ Fixed delay causes thundering herd!
await ctx.scheduler.runAfter(5000, internal.jobs.processWithRetry, {
jobId: args.jobId,
attempt: args.attempt + 1,
});
}
}
return null;
},
});✅ CORRECT:
typescript
export const processWithRetry = internalAction({
args: { jobId: v.id("jobs"), attempt: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
try {
// Process...
} catch (error) {
if (args.attempt < 5) {
// ✅ Exponential backoff + jitter
const baseDelay = Math.pow(2, args.attempt) * 1000;
const jitter = Math.random() * 1000;
await ctx.scheduler.runAfter(
baseDelay + jitter,
internal.jobs.processWithRetry,
{
jobId: args.jobId,
attempt: args.attempt + 1,
}
);
}
}
return null;
},
});❌ 错误示例:
typescript
export const processWithRetry = internalAction({
args: { jobId: v.id("jobs"), attempt: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
try {
// 处理任务...
} catch (error) {
if (args.attempt < 5) {
// ❌ 固定延迟会导致惊群效应!
await ctx.scheduler.runAfter(5000, internal.jobs.processWithRetry, {
jobId: args.jobId,
attempt: args.attempt + 1,
});
}
}
return null;
},
});✅ 正确示例:
typescript
export const processWithRetry = internalAction({
args: { jobId: v.id("jobs"), attempt: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
try {
// 处理任务...
} catch (error) {
if (args.attempt < 5) {
// ✅ 指数退避 + 抖动
const baseDelay = Math.pow(2, args.attempt) * 1000;
const jitter = Math.random() * 1000;
await ctx.scheduler.runAfter(
baseDelay + jitter,
internal.jobs.processWithRetry,
{
jobId: args.jobId,
attempt: args.attempt + 1,
}
);
}
}
return null;
},
});Agent Rules Summary
Agent规则总结
Must Do
必须遵守
- Always include validator on every function
returns - Always use indexes instead of
.filter() - Always use for potentially large queries
take(n) - Always use for document ID arguments
v.id("table") - Always use /
internalMutationfor sensitive operationsinternalAction - Always handle errors in actions and update status in database
- Always use exponential backoff with jitter for retries
- 所有函数必须包含验证器
returns - 始终使用索引而非
.filter() - **始终使用**处理可能返回大量数据的查询
take(n) - **始终使用**定义文档ID类型的参数
v.id("table") - **始终使用/
internalMutation**处理敏感操作internalAction - 始终在Action中处理错误并在数据库中更新状态
- 重试时始终使用带抖动的指数退避算法
Must Not Do
绝对禁止
- Never call in mutations
fetch() - Never access in actions
ctx.db - Never use on database queries
.filter() - Never use without limits on large tables
.collect() - Never use (deprecated, use
v.bigint())v.int64() - Never use type (ESLint rule enforced)
any - Never write to hot-spot documents without sharding/workpool
- Never expose dangerous operations as public functions
- Never rely on multiple mutations for atomic operations
- 绝对不要在Mutation中调用
fetch() - 绝对不要在Action中访问
ctx.db - 绝对不要在数据库查询中使用
.filter() - 绝对不要在大表上无限制地使用
.collect() - 绝对不要使用(已废弃,使用
v.bigint()替代)v.int64() - 绝对不要使用类型(ESLint规则强制禁止)
any - 绝对不要在热点文档上直接写入,需使用分片或Workpool
- 绝对不要将危险操作暴露为公共函数
- 绝对不要依赖多个Mutation保证原子性
Quick Checklist
快速检查清单
Before submitting Convex code, verify:
- All functions have validators
returns - All queries use indexes (no )
.filter() - All calls are bounded with
.collect().take(n) - All ID arguments use
v.id("tableName") - External API calls are in actions, not mutations
- Actions use /
ctx.runQueryfor DB accessctx.runMutation - Sensitive operations use internal functions
- No types in the codebase
any - High-write documents use sharding or Workpool
- Retries use exponential backoff with jitter
提交Convex代码前,请验证:
- 所有函数都有验证器
returns - 所有查询都使用索引(无)
.filter() - 所有调用都通过
.collect()限制数量.take(n) - 所有ID类型的参数都使用
v.id("tableName") - 外部API调用都在Action中执行,而非Mutation
- Action通过/
ctx.runQuery访问数据库ctx.runMutation - 敏感操作使用内部函数
- 代码库中无类型
any - 高写入量文档使用分片或Workpool
- 重试逻辑使用带抖动的指数退避算法