-
Notifications
You must be signed in to change notification settings - Fork 3
batch writes #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
batch writes #167
Conversation
📝 WalkthroughWalkthroughAdds client-side write buffering APIs to Aggregate (startBuffering/finishBuffering/flush) that queue write ops and flush them as a single batch; introduces a server-side Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Aggregate
participant Buffer as BufferQueue
participant Server as BatchMutation
participant BTree as BTreeHandlers
Client->>Aggregate: startBuffering()
Aggregate-->>Client: buffering enabled
Client->>Aggregate: insert(ctx, op1)
Aggregate->>Buffer: enqueue op1
Client->>Aggregate: insert(ctx, op2)
Aggregate->>Buffer: enqueue op2
Client->>Aggregate: finishBuffering(ctx)
Aggregate->>Server: submitBatch([op1, op2])
Server->>Server: group ops by namespace
Server->>BTree: getOrCreate tree for namespace A
Server->>BTree: insertHandler(op1, treeArg)
BTree-->>Server: ok
Server->>BTree: insertHandler(op2, treeArg)
BTree-->>Server: ok
Server-->>Aggregate: batch applied
Aggregate->>Buffer: clear queue
Aggregate-->>Client: finish complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/client/buffer.test.ts (1)
20-53: Consider test isolation for the aggregate instance.The
aggregateinstance is shared across botht.runblocks. If the first test block throws before reachingaggregate.buffer(false), the buffering state remains enabled for subsequent tests. Consider creating a fresh instance per test block or wrapping in try/finally to ensure cleanup.src/client/index.ts (2)
39-81: Consider stronger typing forBufferedOperation.Using
anyfor keys, values, and namespaces loses type safety. While this may be intentional to avoid complex generics, it could allow type mismatches that would only be caught at runtime.
101-124: Buffering state is instance-level - document thread-safety expectations.The
isBufferingandoperationQueueare instance fields. If the sameAggregateinstance is used across concurrent mutations (e.g., multiple requests sharing a singleton), the buffering state could interfere. Consider documenting that buffered aggregates should not be shared across concurrent mutation contexts, or make the state context-scoped.example/convex/batchedWrites.ts (2)
23-50: Missing return validators on all mutations.Per coding guidelines, all Convex functions must include return validators. Each mutation in this file returns a value but lacks a
returns:property.Example fix for
basicBatchedWrites:export const basicBatchedWrites = mutation({ args: { count: v.number(), }, + returns: v.object({ + inserted: v.number(), + total: v.number(), + }), handler: async (ctx, { count }) => {Apply similar changes to:
batchedWritesWithOnSuccess:returns: v.object({ queued: v.number() })complexBatchedOperations:returns: v.object({ operations: v.object({ inserts: v.number(), deletes: v.number(), updates: v.number() }) })comparePerformance:returns: v.object({ method: v.string(), count: v.number(), durationMs: v.number() })autoFlushOnRead:returns: v.object({ queued: v.number(), totalInRange: v.number() })batchedWritesWithNamespaces:returns: v.object({ operations: v.number(), namespaces: v.number(), message: v.string() })Also applies to: 82-107, 112-180, 185-226, 231-264, 273-321
40-43: Inconsistent buffer/flush ordering.Here buffering is disabled before flushing (lines 41, 43), but in
complexBatchedOperations(lines 167, 170) the order is reversed (flush first, then disable). For clarity and consistency, consider adopting a uniform pattern—typically flush while still buffered, then disable.- // Disable buffering after we're done - aggregate.buffer(false); // Flush all buffered operations in a single batch await aggregate.flush(ctx); + // Disable buffering after flush + aggregate.buffer(false);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
example/convex/_generated/api.d.tsis excluded by!**/_generated/**src/component/_generated/component.tsis excluded by!**/_generated/**
📒 Files selected for processing (6)
example/convex/batchedWrites.ts(1 hunks)example/convex/convex.config.ts(1 hunks)src/client/buffer.test.ts(1 hunks)src/client/index.ts(17 hunks)src/component/btree.ts(2 hunks)src/component/public.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/convex.config.tsexample/convex/batchedWrites.ts
🧠 Learnings (17)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
src/component/public.tssrc/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
example/convex/convex.config.tssrc/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
example/convex/convex.config.tssrc/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : By default Convex returns documents in ascending `_creationTime` order; use `.order('asc')` or `.order('desc')` to specify order
Applied to files:
example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
example/convex/convex.config.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
example/convex/convex.config.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/crons.{ts,tsx} : Define crons by declaring the top-level `crons` object, calling methods on it, and exporting it as default from `convex/crons.ts` or similar
Applied to files:
example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/convex.config.tssrc/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex
Applied to files:
example/convex/convex.config.tssrc/client/buffer.test.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
src/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
src/client/buffer.test.tssrc/client/index.tssrc/component/btree.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
src/client/buffer.test.tssrc/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`
Applied to files:
src/client/buffer.test.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
src/client/buffer.test.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
src/client/index.ts
🧬 Code graph analysis (6)
src/component/public.ts (1)
src/component/btree.ts (4)
getOrCreateTree(937-973)DEFAULT_MAX_NODE_SIZE(25-25)insertHandler(45-81)deleteHandler(83-113)
example/convex/convex.config.ts (1)
src/component/schema.ts (1)
aggregate(25-28)
src/client/buffer.test.ts (3)
src/client/setup.test.ts (2)
componentSchema(6-6)componentModules(7-7)src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (2)
DirectAggregate(749-867)count(160-173)
src/client/index.ts (1)
src/client/positions.ts (1)
keyToPosition(67-72)
src/component/btree.ts (1)
example/convex/_generated/dataModel.d.ts (1)
Doc(30-33)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (2)
DirectAggregate(749-867)count(160-173)
🪛 GitHub Actions: Test and lint
src/client/buffer.test.ts
[error] 26-26: TypeScript error TS2339: Property 'aggregateDirect' does not exist on type '{ aggregate: ComponentApi; }'.
🪛 GitHub Check: Test and lint
src/client/buffer.test.ts
[failure] 26-26:
Property 'aggregateDirect' does not exist on type '{ aggregate: ComponentApi; }'.
🔇 Additional comments (9)
example/convex/convex.config.ts (1)
12-12: LGTM!The new aggregate registration for
batchedWritesfollows the established pattern and is correctly placed with other aggregate registrations.src/client/index.ts (2)
131-140: Operations are discarded if flush fails.The queue is cleared before the mutation is awaited. If
ctx.runMutationthrows, the buffered operations are lost and cannot be retried. This may be intentional (prevents double-processing), but the caller has no way to recover.Consider either:
- Documenting this behavior in the method's docstring
- Restoring the queue on failure:
async flush(ctx: RunMutationCtx): Promise<void> { if (this.operationQueue.length === 0) { return; } const operations = this.operationQueue; this.operationQueue = []; - await ctx.runMutation(this.component.public.batch, { - operations, - }); + try { + await ctx.runMutation(this.component.public.batch, { + operations, + }); + } catch (e) { + // Restore operations on failure so caller can retry + this.operationQueue = [...operations, ...this.operationQueue]; + throw e; + } }
164-164: Good coverage of flush-before-read across all query paths.All read operations (
count,countBatch,sum,sumBatch,at,atBatch,indexOf,paginate,paginateNamespaces) correctly callflushBeforeRead. Derived methods likemin,max,random, anditeralso benefit through their use of these primitives.src/component/btree.ts (2)
45-57: LGTM!The optional
treeArgparameter cleanly enables tree reuse for batch operations while preserving existing behavior when not provided.
83-95: LGTM!Consistent pattern with
insertHandlerfor the optional tree injection.src/component/public.ts (2)
229-240: Operation order not preserved across namespaces.Grouping by namespace means operations are reordered. For example,
[insert ns:A, insert ns:B, delete ns:A]becomes[insert A, delete A]then[insert B]. If this reordering is intentional for performance, consider documenting it. If original order must be preserved, process operations in input order while caching trees.If order must be preserved:
handler: async (ctx, { operations }) => { - // Group operations by namespace to fetch each tree once - const namespaceGroups = new Map<string, typeof operations>(); - for (const op of operations) { - ... - } - - // Process each namespace group - for (const [namespaceKey, ops] of namespaceGroups.entries()) { - ... - } + // Cache trees by namespace to avoid repeated fetches + const treeCache = new Map<string, Doc<"btree">>(); + + for (const op of operations) { + const namespace = "namespace" in op ? op.namespace : undefined; + const key = namespace === undefined ? "__undefined__" : JSON.stringify(namespace); + + if (!treeCache.has(key)) { + treeCache.set(key, await getOrCreateTree(ctx.db, namespace, DEFAULT_MAX_NODE_SIZE, true)); + } + const tree = treeCache.get(key)!; + + // Process operation with cached tree... + } },
336-363:insertIfDoesNotExistreplaces existing items rather than no-op.The comment notes it's "implemented as replaceOrInsert", which means if the key exists, it gets deleted and re-inserted. This is consistent with the non-batch
_insertIfDoesNotExistimplementation, but the name suggests it should be a no-op if the item exists.Verify this semantic is intentional. If true "insert if not exists" is needed:
} else if (op.type === "insertIfDoesNotExist") { - // insertIfDoesNotExist is implemented as replaceOrInsert - try { - await deleteHandler(...); - } catch (e) { ... } - await insertHandler(...); + // True insert-if-not-exists: try insert, ignore duplicate key error + try { + await insertHandler( + ctx, + { + key: op.key, + value: op.value, + summand: op.summand, + namespace: op.namespace, + }, + tree, + ); + } catch (e) { + if (!(e instanceof ConvexError && e.data?.code === "DUPLICATE_KEY")) { + throw e; + } + }example/convex/batchedWrites.ts (2)
286-290: Good pattern: local aggregate instance.Creating the
DirectAggregateinside the handler avoids shared mutable state issues. This is the recommended approach compared to the module-level instance used elsewhere in this file.
15-18: MoveDirectAggregateinstantiation into each mutation handler.Module-level mutable state violates Convex's execution model. The
aggregateinstance maintains state viathis.isBufferingandthis.operationQueue, which are not reliably preserved across function invocations. Convex executes mutations in isolated contexts and may re-run or move functions between runtimes, causing module-level state to be lost and breaking transactional guarantees. InstantiateDirectAggregateinside each handler instead, as done inbatchedWritesWithNamespaces(lines 286–290), so each invocation has its own isolated state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In @example/convex/batchedWrites.ts:
- Around line 38-63: The mutation basicBatchedWrites is missing a returns
validator and also fails to await the async aggregate.finishBuffering call; add
a returns validator to the mutation (matching the returned object shape, e.g.,
inserted and total as numbers) via the mutation({ args: ..., returns: ... })
signature and change the finishBuffering call to await
aggregate.finishBuffering(ctx) so the buffering completes before reading
aggregate.count(ctx).
In @src/client/index.ts:
- Around line 148-159: The error thrown in flushBeforeRead references a
non-existent .buffer(false) method; update the message to reference the real
API: mention finishBuffering() or flush() instead. In the flushBeforeRead method
(check isBuffering, operationQueue, and the "runMutation" in ctx branch),
replace the string that suggests ".buffer(false)" with a corrected instruction
such as calling finishBuffering() or flush() before reading so the error message
matches available methods.
- Around line 106-121: The docstring for startBuffering() incorrectly references
stopBuffering(); update the documentation example to call finishBuffering()
instead (e.g., replace "await aggregate.stopBuffering(ctx)" with "await
aggregate.finishBuffering(ctx)") and ensure the docstring text and example
consistently use the actual method name finishBuffering() so consumers see the
correct API.
🧹 Nitpick comments (7)
example/convex/batchedWrites.ts (7)
114-135: Missing return validator.Per coding guidelines, all Convex functions should include return validators.
📝 Suggested fix
export const addMultipleScores = mutationWithTriggers({ args: { scores: v.array( v.object({ name: v.string(), score: v.number(), }), ), }, + returns: v.object({ + inserted: v.number(), + message: v.string(), + }), handler: async (ctx, { scores }) => {
165-208: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const compareTriggersWithAndWithoutBatching = mutation({ args: { count: v.number(), useBatching: v.boolean(), }, + returns: v.object({ + method: v.string(), + count: v.number(), + durationMs: v.number(), + message: v.string(), + }), handler: async (ctx, { count, useBatching }) => {
213-278: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const complexBatchedOperations = mutation({ args: { inserts: v.array( v.object({ key: v.number(), id: v.string(), value: v.number(), }), ), deletes: v.array( v.object({ key: v.number(), id: v.string(), }), ), updates: v.array( v.object({ oldKey: v.number(), newKey: v.number(), id: v.string(), value: v.number(), }), ), }, + returns: v.object({ + operations: v.object({ + inserts: v.number(), + deletes: v.number(), + updates: v.number(), + }), + }), handler: async (ctx, { inserts, deletes, updates }) => {
283-323: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const comparePerformance = mutation({ args: { count: v.number(), useBatching: v.boolean(), }, + returns: v.object({ + method: v.string(), + count: v.number(), + durationMs: v.number(), + }), handler: async (ctx, { count, useBatching }) => {
328-361: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const autoFlushOnRead = mutation({ args: { count: v.number(), }, + returns: v.object({ + queued: v.number(), + totalInRange: v.number(), + }), handler: async (ctx, { count }) => {
370-415: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const batchedWritesWithNamespaces = mutation({ args: { operations: v.array( v.object({ namespace: v.string(), key: v.number(), id: v.string(), value: v.number(), }), ), }, + returns: v.object({ + operations: v.number(), + namespaces: v.number(), + message: v.string(), + }), handler: async (ctx, { operations }) => {
140-160: Missing return validator.Per coding guidelines, this mutation should include a
returnsvalidator.📝 Suggested fix
export const updateMultipleScores = mutationWithTriggers({ args: { updates: v.array( v.object({ id: v.id("leaderboard"), newScore: v.number(), }), ), }, + returns: v.object({ + updated: v.number(), + message: v.string(), + }), handler: async (ctx, { updates }) => {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
README.mdexample/convex/batchedWrites.tssrc/client/index.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/batchedWrites.ts
🧠 Learnings (14)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
src/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
src/client/index.tsREADME.mdexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.
Applied to files:
example/convex/batchedWrites.ts
🧬 Code graph analysis (2)
src/client/index.ts (2)
src/component/_generated/component.ts (1)
ComponentApi(24-288)src/client/positions.ts (1)
keyToPosition(67-72)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (3)
DirectAggregate(755-873)TableAggregate(908-1059)count(166-179)
🔇 Additional comments (7)
README.md (2)
492-559: Documentation looks good for the new batching feature.The documentation clearly explains the benefits of batching with triggers and provides a complete, working example. The pattern shown aligns with the implementation in
src/client/index.ts.
618-656: Well-documented batch write operations section.The documentation clearly explains the batching API, its benefits, and provides a simple usage example. The cross-references to other sections and the example file are helpful.
src/client/index.ts (4)
39-81: BufferedOperation type definition looks correct.The discriminated union covers all mutation types (insert, delete, replace, deleteIfExists, replaceOrInsert, insertIfDoesNotExist) and matches the batch operation schema in
src/component/_generated/component.tslines 157-193.
137-146: Consider error handling behavior in flush.If
ctx.runMutationfails after the queue is cleared (line 142), the buffered operations are lost. This may be intentional (mutations in Convex are retried by the runtime), but if the mutation is abandoned after retries, the operations won't be persisted.Since Convex handles retries at the mutation level and the entire mutation would be retried from the beginning (re-populating the buffer), this behavior is likely correct. However, it's worth confirming this is the intended semantics.
486-502: Write operation buffering implementation looks correct.The pattern of checking
isBufferingand either queueing or executing directly is consistent across all write methods. The queued operations use the samekeyToPositiontransformation as the direct mutation calls.
166-179: Read operations correctly flush before executing.All read methods (
count,countBatch,sum,sumBatch,at,atBatch,indexOf,paginate,paginateNamespaces) properly awaitflushBeforeRead(ctx)before executing, ensuring read-after-write consistency when buffering is enabled.example/convex/batchedWrites.ts (1)
88-106: Custom mutation pattern looks correct.This demonstrates the recommended pattern for combining triggers with buffering. The buffering is enabled before the mutation runs and flushed in the
onSuccesscallback.
a40535e to
0d25f49
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🤖 Fix all issues with AI agents
In @example/convex/batchedWrites.ts:
- Around line 35-40: The handler is creating a floating promise by calling
finishBuffering without awaiting it, so the subsequent count() (and other reads)
can observe unflushed writes; update the handlers in
example/convex/batchedWrites.ts (including the reset mutation and the other
mutation(s) around lines 45–77) to await finishBuffering before proceeding or
returning (e.g., await finishBuffering(...)) so flushing completes before
calling count() or finishing the mutation.
- Around line 1-10: Update the example header to reference the new public API
names instead of the old `.buffer()` call: change the text to mention
startBuffering, finishBuffering and flush as the methods to queue and flush
batched write operations (e.g., "use startBuffering/finishBuffering/flush to
queue and flush writes"), and ensure any explanatory sentences that describe
`.buffer()` behavior are updated to describe the new
startBuffering/finishBuffering/flush workflow used in this file
(batchedWrites.ts).
- Around line 35-439: Several exported mutations in this file are missing the
required returns validators; update each exported call to include a returns:
validator matching the actual return shape (or v.null() if nothing is returned).
For example, add returns: v.null() to reset and other void-returning mutations,
add returns: v.object({ inserted: v.number(), total: v.number() }) to
basicBatchedWrites, add appropriate object validators to addMultipleScores,
updateMultipleScores, complexBatchedOperations, comparePerformance,
autoFlushOnRead, batchedWritesWithNamespaces, and
compareTriggersWithAndWithoutBatching, and ensure
mutationWithTriggers/customMutation declarations also include returns when they
export a handler that returns a value; locate these by the exported symbols
reset, basicBatchedWrites, mutationWithTriggers, addMultipleScores,
updateMultipleScores, compareTriggersWithAndWithoutBatching,
complexBatchedOperations, comparePerformance, autoFlushOnRead, and
batchedWritesWithNamespaces and add the matching returns validators.
In @src/client/index.ts:
- Around line 120-123: finishBuffering currently sets isBuffering = false before
awaiting flush(), which allows concurrent reads to bypass flushBeforeRead while
writes are still pending; change the order so you await this.flush(ctx) first
and only set this.isBuffering = false after the flush completes successfully (do
not swallow flush errors so buffering remains enabled on failure); reference:
finishBuffering, isBuffering, flush, and flushBeforeRead.
- Around line 141-152: The error string in flushBeforeRead references a
non-existent .buffer(false); update the message in flushBeforeRead to accurately
instruct users: when this.isBuffering && this.operationQueue.length > 0 and the
ctx is a query (no runMutation), throw an Error that tells callers they cannot
read with buffered operations and should either call flush(ctx) before reading,
finishBuffering() to stop buffering, or perform the action from a mutation
context; keep references to flushBeforeRead, isBuffering, operationQueue, flush,
finishBuffering, runMutation, and the RunQueryCtx/RunMutationCtx types so
reviewers can locate and verify the change.
- Around line 120-139: flush() currently clears this.operationQueue before
calling ctx.runMutation, losing ops if runMutation throws; change flush to
preserve the queue on failure by capturing the ops to send (e.g., const
operations = this.operationQueue.slice()) and only mutate this.operationQueue
(clear or remove those sent) after runMutation succeeds; use try/catch to
rethrow errors while leaving this.operationQueue intact on failure and reference
the methods/fields: flush, finishBuffering, this.operationQueue,
ctx.runMutation, and this.component.public.batch to locate where to implement
the change.
- Around line 99-123: The docstrings reference old API names (`stopBuffering`,
`.buffer(false)`) that no longer exist after the rename to
startBuffering/finishBuffering; update the example and any mentions in the
comments to call finishBuffering (and/or await aggregate.finishBuffering(ctx))
and remove references to `.buffer(false)`, and ensure the narrative mentions
that finishBuffering will call flush(ctx) to send queued operations (refer to
startBuffering, finishBuffering, flush, and RunMutationCtx to locate the code).
In @src/component/public.ts:
- Around line 315-331: The replace branch is not forwarding op.namespace and
op.newNamespace into the args passed to replaceHandler, so replaceHandler (and
downstream deleteHandler/insertHandler) lose namespace context; update the call
to replaceHandler in the block handling op.type === "replace" to include
namespace and newNamespace fields (e.g., add namespace: op.namespace and
newNamespace: op.newNamespace to the second argument), while still resolving
deleteTree/insertTree via
getTreeForNamespace(op.namespace)/getTreeForNamespace(op.newNamespace) so that
replaceHandler and its deleteHandler/insertHandler children receive the correct
namespace values for behavior, logging, and invariants.
- Around line 230-289: getTreeForNamespace currently uses
JSON.stringify(namespace) which throws on BigInt; update it to serialize
namespaces via convexToJson before stringifying so keys handle BigInt and other
Convex types. Replace JSON.stringify(namespace) with
JSON.stringify(convexToJson(namespace)) when computing the sentinel key and keep
the existing sentinel for undefined; ensure you still call
getOrCreateTree(ctx.db, namespace, DEFAULT_MAX_NODE_SIZE, true) and store its
ReturnType in treesMap keyed by the new serialized key.
🧹 Nitpick comments (1)
src/client/index.ts (1)
39-75: Avoidanyfor buffered ops (at least useunknown/ shared Key types).
BufferedOperationbeingany-heavy weakens the main value of batching (catching shape mismatches at compile time). Consider typingkeyasKey,namespaceasConvexValue | undefined, andvalueasID(orunknown) where possible.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
example/convex/_generated/api.d.tsis excluded by!**/_generated/**src/component/_generated/component.tsis excluded by!**/_generated/**
📒 Files selected for processing (7)
README.mdexample/convex/batchedWrites.tsexample/convex/convex.config.tssrc/client/buffer.test.tssrc/client/index.tssrc/component/btree.tssrc/component/public.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/client/buffer.test.ts
- example/convex/convex.config.ts
- src/component/btree.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/batchedWrites.ts
🧠 Learnings (18)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
README.mdsrc/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
src/component/public.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
src/component/public.tssrc/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
src/component/public.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Applied to files:
src/component/public.tssrc/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
src/component/public.tssrc/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the database
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex storage stores items as `Blob` objects; convert all items to/from a `Blob` when using Convex storage
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
src/component/public.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
src/component/public.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
src/component/public.tssrc/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.
Applied to files:
example/convex/batchedWrites.ts
🧬 Code graph analysis (2)
src/component/public.ts (1)
src/component/btree.ts (7)
Key(27-27)Value(28-28)Namespace(29-29)deleteHandler(83-113)insertHandler(45-81)getOrCreateTree(949-985)DEFAULT_MAX_NODE_SIZE(25-25)
src/client/index.ts (2)
src/component/_generated/component.ts (1)
ComponentApi(24-281)src/client/positions.ts (1)
keyToPosition(70-75)
🪛 GitHub Actions: Test and lint
example/convex/batchedWrites.ts
[error] 65-65: ESLint: no-floating-promises violation. Promises must be awaited, end with a call to .catch or .then with a rejection handler, or be explicitly marked as ignored with the void operator.
🪛 GitHub Check: Test and lint
example/convex/batchedWrites.ts
[failure] 65-65:
Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator
🔇 Additional comments (8)
src/client/index.ts (2)
159-324: Auto-flush-before-read integration looks consistent.
Good coverage:count/sum/at/indexOf/paginate/paginateNamespacesall gate reads throughflushBeforeRead, which matches the buffering contract.Also applies to: 398-429, 654-673
471-610: Buffered write enqueuing is straightforward and preserves operation order.
The enqueue shapes match the component batch API (insert/delete/replace/deleteIfExists/replaceOrInsert).src/component/public.ts (3)
74-118: Handler extraction + optional tree injection is a clean batch-enabler.
replaceHandlerdelegating todeleteHandler+insertHandlerwith injectable trees is a good reuse point for the new batch API.
226-358: Batch op loop preserves caller order; reuse of per-namespace trees is clear.
The sequential processing matches the expectation of deterministic “same order as provided” semantics while still removing repeated tree fetches.
125-138: The error code"DELETE_MISSING_KEY"is the canonical code used in this codebase. It's defined insrc/component/btree.ts:541and thrown bydeleteFromNodewhen a key is not found. The error handling in thedeleteIfExistsHandlerfunction correctly catches this error and suppresses it as intended.example/convex/batchedWrites.ts (2)
94-233: The example coverage is nice (direct batching, trigger batching, namespaces).
Once theawait+returnsissues are fixed, this should be a solid reference for users.Also applies to: 234-347, 349-439
102-121: UsecustomCtxhelper to preserve full context when wrapping with triggers.The pattern at lines 102-120 should import and use
customCtx(triggers.wrapDB)instead of manually reconstructing the context with only...triggers.wrapDB(ctx). Follow the established pattern inleaderboard.ts(line 53) andphotos.ts(line 40):const mutationWithTriggers = customMutation( mutation, customCtx(triggers.wrapDB), );Import
customCtxfrom"convex-helpers/server/customFunctions"and apply it to the wrapper function. The current manual reconstruction loses context properties beyond the wrapped db, which can cause issues if the handler or aggregate methods need access to other context functionality.Also applies to: 128-185
⛔ Skipped due to learnings
Learnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the databaseLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or actionLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return valueLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not existLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or actionLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runAction` to call an action from another action; otherwise pull out shared code into a helper async functionLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not existLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functionsLearnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : Do NOT use deprecated `ctx.storage.getMetadata`; instead query the `_storage` system table using `ctx.db.system.get`Learnt from: CR Repo: get-convex/aggregate PR: 0 File: example/.cursor/rules/convex_rules.mdc:0-0 Timestamp: 2025-12-03T18:51:23.434Z Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` propertyREADME.md (1)
578-657: Batch read/write docs align with the new API naming.
The renamed “Batch Read Operations” and the new “Batch Write Operations” section matchcountBatch/sumBatch/atBatchandstartBuffering/finishBufferingusage.
| export const reset = internalMutation({ | ||
| args: {}, | ||
| handler: async (ctx) => { | ||
| await aggregate.clearAll(ctx); | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Basic example: Enable buffering, queue operations, then flush manually. | ||
| */ | ||
| export const basicBatchedWrites = internalMutation({ | ||
| args: { | ||
| count: v.number(), | ||
| }, | ||
| handler: async (ctx, { count }) => { | ||
| // Enable buffering mode - modifies the aggregate instance in place | ||
| aggregate.startBuffering(); | ||
|
|
||
| const initialCount = await aggregate.count(ctx); | ||
|
|
||
| // Queue multiple insert operations | ||
| for (let i = 0; i < count; i++) { | ||
| await aggregate.insert(ctx, { | ||
| key: i + initialCount, | ||
| id: `item-${i}`, | ||
| sumValue: i * 10, | ||
| }); | ||
| } | ||
|
|
||
| // Disable buffering after we're done | ||
| aggregate.finishBuffering(ctx); | ||
|
|
||
| // Read operations work normally (and auto-flush if needed) | ||
| const total = await aggregate.count(ctx); | ||
|
|
||
| if (total !== initialCount + count) { | ||
| console.log({ initialCount, count, total }); | ||
| throw new Error("Total count is incorrect"); | ||
| } | ||
|
|
||
| return { inserted: count, total }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Advanced example: Use custom functions with Triggers and buffering. | ||
| * | ||
| * This is the RECOMMENDED pattern when using triggers! | ||
| * | ||
| * When using triggers, each table write triggers an aggregate write. | ||
| * If you insert 100 rows, that's 100 separate calls to the aggregate component. | ||
| * With buffering, all 100 writes are batched into a single component call. | ||
| * | ||
| * Performance benefits: | ||
| * - Single component call instead of N calls | ||
| * - Single tree fetch instead of N fetches | ||
| * - Better handling of write contention | ||
| */ | ||
|
|
||
| // Set up triggers | ||
| const triggers = new Triggers<DataModel>(); | ||
| triggers.register("leaderboard", leaderboardAggregate.trigger()); | ||
|
|
||
| // Create a custom mutation that: | ||
| // 1. Wraps the database with triggers | ||
| // 2. Enables buffering before the mutation runs | ||
| // 3. Flushes after the mutation completes successfully | ||
| const mutationWithTriggers = customMutation(mutation, { | ||
| args: {}, | ||
| input: async (ctx) => { | ||
| // Enable buffering for all aggregate operations | ||
| leaderboardAggregate.startBuffering(); | ||
|
|
||
| return { | ||
| ctx: { | ||
| // Wrap db with triggers | ||
| ...triggers.wrapDB(ctx), | ||
| }, | ||
| args: {}, | ||
| onSuccess: async ({ ctx }) => { | ||
| // Flush all buffered operations in a single batch | ||
| await leaderboardAggregate.finishBuffering(ctx); | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Example: Add multiple scores with triggers and batching. | ||
| * | ||
| * Without buffering: Each insert triggers a separate aggregate.insert call | ||
| * With buffering: All inserts are batched into one aggregate.batch call | ||
| */ | ||
| export const addMultipleScores = mutationWithTriggers({ | ||
| args: { | ||
| scores: v.array( | ||
| v.object({ | ||
| name: v.string(), | ||
| score: v.number(), | ||
| }), | ||
| ), | ||
| }, | ||
| handler: async (ctx, { scores }) => { | ||
| const initialSumValue = await leaderboardAggregate.sum(ctx); | ||
|
|
||
| // Just insert into the table - the trigger automatically | ||
| // updates the aggregate, and buffering batches all the updates | ||
| for (const { name, score } of scores) { | ||
| await ctx.db.insert("leaderboard", { name, score }); | ||
| } | ||
|
|
||
| const totalSumValue = await leaderboardAggregate.sum(ctx); | ||
|
|
||
| if ( | ||
| totalSumValue !== | ||
| initialSumValue + scores.reduce((acc, { score }) => acc + score, 0) | ||
| ) { | ||
| throw new Error("Total sum value is incorrect"); | ||
| } | ||
|
|
||
| return { | ||
| inserted: scores.length, | ||
| message: `Added ${scores.length} scores with batched aggregate updates`, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Example: Update multiple scores - shows replace operations are also batched | ||
| */ | ||
| export const updateMultipleScores = mutationWithTriggers({ | ||
| args: { | ||
| updates: v.array( | ||
| v.object({ | ||
| id: v.id("leaderboard"), | ||
| newScore: v.number(), | ||
| }), | ||
| ), | ||
| }, | ||
| handler: async (ctx, { updates }) => { | ||
| // Each patch triggers aggregate.replace, all batched together | ||
| for (const { id, newScore } of updates) { | ||
| await ctx.db.patch(id, { score: newScore }); | ||
| } | ||
|
|
||
| return { | ||
| updated: updates.length, | ||
| message: `Updated ${updates.length} scores with batched aggregate updates`, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Example showing the difference with and without batching | ||
| */ | ||
| export const compareTriggersWithAndWithoutBatching = mutation({ | ||
| args: { | ||
| count: v.number(), | ||
| useBatching: v.boolean(), | ||
| }, | ||
| handler: async (ctx, { count, useBatching }) => { | ||
| console.time(); | ||
|
|
||
| const customCtx = triggers.wrapDB(ctx); | ||
| if (useBatching) { | ||
| // With batching: all aggregate operations batched into one call | ||
| leaderboardAggregate.startBuffering(); | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| await customCtx.db.insert("leaderboard", { | ||
| name: `player-${i}`, | ||
| score: Math.floor(Math.random() * 1000), | ||
| }); | ||
| } | ||
|
|
||
| await leaderboardAggregate.finishBuffering(ctx); | ||
| } else { | ||
| // Without batching: each insert makes a separate aggregate call | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| await customCtx.db.insert("leaderboard", { | ||
| name: `player-${i}`, | ||
| score: Math.floor(Math.random() * 1000), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| console.timeEnd(); | ||
|
|
||
| return { | ||
| method: useBatching ? "with batching" : "without batching", | ||
| count, | ||
| message: useBatching | ||
| ? `1 batched call to aggregate component` | ||
| : `${count} individual calls to aggregate component`, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Complex example: Mix different operation types in a batch. | ||
| */ | ||
| export const complexBatchedOperations = mutation({ | ||
| args: { | ||
| inserts: v.array( | ||
| v.object({ | ||
| key: v.number(), | ||
| id: v.string(), | ||
| value: v.number(), | ||
| }), | ||
| ), | ||
| deletes: v.array( | ||
| v.object({ | ||
| key: v.number(), | ||
| id: v.string(), | ||
| }), | ||
| ), | ||
| updates: v.array( | ||
| v.object({ | ||
| oldKey: v.number(), | ||
| newKey: v.number(), | ||
| id: v.string(), | ||
| value: v.number(), | ||
| }), | ||
| ), | ||
| }, | ||
| handler: async (ctx, { inserts, deletes, updates }) => { | ||
| // Enable buffering | ||
| aggregate.startBuffering(); | ||
|
|
||
| // Queue inserts | ||
| for (const item of inserts) { | ||
| await aggregate.insert(ctx, { | ||
| key: item.key, | ||
| id: item.id, | ||
| sumValue: item.value, | ||
| }); | ||
| } | ||
|
|
||
| // Queue deletes | ||
| for (const item of deletes) { | ||
| await aggregate.deleteIfExists(ctx, { | ||
| key: item.key, | ||
| id: item.id, | ||
| }); | ||
| } | ||
|
|
||
| // Queue updates (replace operations) | ||
| for (const item of updates) { | ||
| await aggregate.replaceOrInsert( | ||
| ctx, | ||
| { key: item.oldKey, id: item.id }, | ||
| { key: item.newKey, sumValue: item.value }, | ||
| ); | ||
| } | ||
|
|
||
| // Flush all operations at once and stop buffering | ||
| await aggregate.finishBuffering(ctx); | ||
|
|
||
| return { | ||
| operations: { | ||
| inserts: inserts.length, | ||
| deletes: deletes.length, | ||
| updates: updates.length, | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Performance comparison: Batched vs unbatched writes. | ||
| */ | ||
| export const comparePerformance = mutation({ | ||
| args: { | ||
| count: v.number(), | ||
| useBatching: v.boolean(), | ||
| }, | ||
| handler: async (ctx, { count, useBatching }) => { | ||
| const start = Date.now(); | ||
|
|
||
| if (useBatching) { | ||
| // Batched approach | ||
| aggregate.startBuffering(); | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| await aggregate.insert(ctx, { | ||
| key: 1000000 + i, | ||
| id: `perf-test-${i}`, | ||
| sumValue: i, | ||
| }); | ||
| } | ||
|
|
||
| await aggregate.finishBuffering(ctx); | ||
| } else { | ||
| // Unbatched approach | ||
| for (let i = 0; i < count; i++) { | ||
| await aggregate.insert(ctx, { | ||
| key: 1000000 + i, | ||
| id: `perf-test-${i}`, | ||
| sumValue: i, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const duration = Date.now() - start; | ||
|
|
||
| return { | ||
| method: useBatching ? "batched" : "unbatched", | ||
| count, | ||
| durationMs: duration, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Example showing automatic flush on read operations. | ||
| */ | ||
| export const autoFlushOnRead = mutation({ | ||
| args: { | ||
| count: v.number(), | ||
| }, | ||
| handler: async (ctx, { count }) => { | ||
| // Enable buffering | ||
| aggregate.startBuffering(); | ||
|
|
||
| // Queue some operations | ||
| for (let i = 0; i < count; i++) { | ||
| await aggregate.insert(ctx, { | ||
| key: 2000000 + i, | ||
| id: `auto-flush-${i}`, | ||
| sumValue: i, | ||
| }); | ||
| } | ||
|
|
||
| // This read operation automatically flushes the buffer first | ||
| // So we'll see the correct count including the queued operations | ||
| const total = await aggregate.count(ctx, { | ||
| bounds: { | ||
| lower: { key: 2000000, inclusive: true }, | ||
| }, | ||
| }); | ||
|
|
||
| // Flush all operations at once and stop buffering | ||
| await aggregate.finishBuffering(ctx); | ||
|
|
||
| return { | ||
| queued: count, | ||
| totalInRange: total, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| /** | ||
| * Example: Batch operations with namespace grouping. | ||
| * | ||
| * When you have operations across multiple namespaces, | ||
| * the batch mutation automatically groups them and fetches | ||
| * each namespace's tree only once. | ||
| */ | ||
| export const batchedWritesWithNamespaces = mutation({ | ||
| args: { | ||
| operations: v.array( | ||
| v.object({ | ||
| namespace: v.string(), | ||
| key: v.number(), | ||
| id: v.string(), | ||
| value: v.number(), | ||
| }), | ||
| ), | ||
| }, | ||
| handler: async (ctx, { operations }) => { | ||
| // Create a namespaced aggregate | ||
| const namespacedAggregate = new DirectAggregate<{ | ||
| Key: number; | ||
| Id: string; | ||
| Namespace: string; | ||
| }>(components.batchedWrites); | ||
|
|
||
| // Enable buffering | ||
| namespacedAggregate.startBuffering(); | ||
|
|
||
| // Queue operations - they'll be grouped by namespace internally | ||
| for (const op of operations) { | ||
| await namespacedAggregate.insert(ctx, { | ||
| namespace: op.namespace, | ||
| key: op.key, | ||
| id: op.id, | ||
| sumValue: op.value, | ||
| }); | ||
| } | ||
|
|
||
| // Flush all operations and stop buffering | ||
| // The batch mutation will group by namespace automatically | ||
| await namespacedAggregate.finishBuffering(ctx); | ||
|
|
||
| // Count unique namespaces | ||
| const namespaces = new Set(operations.map((op) => op.namespace)); | ||
|
|
||
| return { | ||
| operations: operations.length, | ||
| namespaces: namespaces.size, | ||
| message: `Processed ${operations.length} operations across ${namespaces.size} namespaces in a single batch`, | ||
| }; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add returns validators to all exported functions in example/convex/**.
Per the repo’s example/convex guidelines, every mutation / internalMutation must declare both args and returns (use returns: v.null() if nothing is returned). This file currently omits returns in many exports (e.g. reset, basicBatchedWrites, compareTriggersWithAndWithoutBatching, etc.).
Based on coding guidelines, add returns throughout (e.g. basicBatchedWrites could return v.object({ inserted: v.number(), total: v.number() }), others v.null() or appropriate objects).
🧰 Tools
🪛 GitHub Actions: Test and lint
[error] 65-65: ESLint: no-floating-promises violation. Promises must be awaited, end with a call to .catch or .then with a rejection handler, or be explicitly marked as ignored with the void operator.
🪛 GitHub Check: Test and lint
[failure] 65-65:
Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator
🤖 Prompt for AI Agents
In @example/convex/batchedWrites.ts around lines 35 - 439, Several exported
mutations in this file are missing the required returns validators; update each
exported call to include a returns: validator matching the actual return shape
(or v.null() if nothing is returned). For example, add returns: v.null() to
reset and other void-returning mutations, add returns: v.object({ inserted:
v.number(), total: v.number() }) to basicBatchedWrites, add appropriate object
validators to addMultipleScores, updateMultipleScores, complexBatchedOperations,
comparePerformance, autoFlushOnRead, batchedWritesWithNamespaces, and
compareTriggersWithAndWithoutBatching, and ensure
mutationWithTriggers/customMutation declarations also include returns when they
export a handler that returns a value; locate these by the exported symbols
reset, basicBatchedWrites, mutationWithTriggers, addMultipleScores,
updateMultipleScores, compareTriggersWithAndWithoutBatching,
complexBatchedOperations, comparePerformance, autoFlushOnRead, and
batchedWritesWithNamespaces and add the matching returns validators.
| ### Optimizing Triggers with Batching | ||
|
|
||
| **Recommended:** When using triggers, combine them with the batching API for | ||
| optimal performance. | ||
|
|
||
| Without batching, each table write triggers a separate call to the aggregate | ||
| component. If you insert 100 rows in a mutation, that's 100 individual calls to | ||
| the aggregate component, each fetching and updating the B-tree separately. | ||
|
|
||
| With batching, all triggered aggregate operations are queued and sent as a | ||
| single batch at the end of the mutation. This provides: | ||
|
|
||
| - **Single component call** instead of N separate calls | ||
| - **Single tree fetch** instead of N fetches | ||
| - **Better write contention handling** - one atomic update instead of many | ||
| - **Significant performance improvement** - especially for bulk operations | ||
|
|
||
| Here's how to set it up: | ||
|
|
||
| ```ts | ||
| import { TableAggregate } from "@convex-dev/aggregate"; | ||
| import { Triggers } from "convex-helpers/server/triggers"; | ||
| import { customMutation } from "convex-helpers/server/customFunctions"; | ||
| import { mutation as rawMutation } from "./_generated/server"; | ||
|
|
||
| const aggregate = new TableAggregate<{ | ||
| Key: number; | ||
| DataModel: DataModel; | ||
| TableName: "leaderboard"; | ||
| }>(components.aggregate, { | ||
| sortKey: (doc) => -doc.score, | ||
| }); | ||
|
|
||
| // Set up triggers | ||
| const triggers = new Triggers<DataModel>(); | ||
| triggers.register("leaderboard", aggregate.trigger()); | ||
|
|
||
| // Create a custom mutation that enables buffering and flushes on success | ||
| const mutation = customMutation(rawMutation, { | ||
| args: {}, | ||
| input: async (ctx) => { | ||
| aggregate.startBuffering(); | ||
| return { | ||
| ctx: triggers.wrapDB(ctx), | ||
| args: {}, | ||
| onSuccess: async ({ ctx }) => { | ||
| await aggregate.finishBuffering(ctx); | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| // Now use this mutation in your functions | ||
| export const addScores = mutation({ | ||
| args: { scores: v.array(v.object({ name: v.string(), score: v.number() })) }, | ||
| handler: async (ctx, { scores }) => { | ||
| // Each insert triggers an aggregate operation, but they're all batched! | ||
| for (const { name, score } of scores) { | ||
| await ctx.db.insert("leaderboard", { name, score }); | ||
| } | ||
| // The flush happens automatically in the onSuccess callback | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| See [`example/convex/batchedWrites.ts`](example/convex/batchedWrites.ts) for a | ||
| complete working example with performance comparisons. | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Batching-with-triggers snippet is missing key imports/context (components, DataModel, v).
In the added block, the snippet references components.aggregate, DataModel, and uses v later, but doesn’t show those imports (or where they come from). Consider adding the missing imports or a short comment that it’s an excerpt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (10)
example/convex/batchedWrites.ts (10)
35-40: Add return validator for consistency with coding guidelines.The function should include
returns: v.null()since it doesn't return a value. This aligns with the guideline to always include return validators for all Convex functions.♻️ Add return validator
export const reset = internalMutation({ args: {}, + returns: v.null(), handler: async (ctx) => { await aggregate.clearAll(ctx); }, });Based on coding guidelines.
45-77: Add return validator for type safety.The function returns an object with
insertedandtotalproperties but lacks a return validator. Adding one improves type safety and aligns with coding guidelines.♻️ Add return validator
export const basicBatchedWrites = internalMutation({ args: { count: v.number(), }, + returns: v.object({ + inserted: v.number(), + total: v.number(), + }), handler: async (ctx, { count }) => {Based on coding guidelines.
128-160: Add return validator for consistency.The function returns an object with
insertedandmessageproperties but lacks a return validator.♻️ Add return validator
export const addMultipleScores = mutationWithTriggers({ args: { scores: v.array( v.object({ name: v.string(), score: v.number(), }), ), }, + returns: v.object({ + inserted: v.number(), + message: v.string(), + }), handler: async (ctx, { scores }) => {Based on coding guidelines.
165-185: Add return validator for consistency.The function returns an object with
updatedandmessageproperties but lacks a return validator.♻️ Add return validator
export const updateMultipleScores = mutationWithTriggers({ args: { updates: v.array( v.object({ id: v.id("leaderboard"), newScore: v.number(), }), ), }, + returns: v.object({ + updated: v.number(), + message: v.string(), + }), handler: async (ctx, { updates }) => {Based on coding guidelines.
196-196: Add label to console.time for clarity.Using
console.time()without a label makes it unclear which timer is being measured, especially if multiple timers run concurrently. Add a descriptive label.♻️ Add timer label
- console.time(); + console.time("compareTriggersWithAndWithoutBatching");And correspondingly update the timeEnd call on line 222:
- console.timeEnd(); + console.timeEnd("compareTriggersWithAndWithoutBatching");
190-232: Add return validator for consistency.The function returns an object with
method,count, andmessageproperties but lacks a return validator.♻️ Add return validator
export const compareTriggersWithAndWithoutBatching = mutation({ args: { count: v.number(), useBatching: v.boolean(), }, + returns: v.object({ + method: v.string(), + count: v.number(), + message: v.string(), + }), handler: async (ctx, { count, useBatching }) => {Based on coding guidelines.
237-302: Add return validator for consistency.The function returns an object with a nested
operationsobject but lacks a return validator.♻️ Add return validator
export const complexBatchedOperations = mutation({ args: { inserts: v.array( v.object({ key: v.number(), id: v.string(), value: v.number(), }), ), deletes: v.array( v.object({ key: v.number(), id: v.string(), }), ), updates: v.array( v.object({ oldKey: v.number(), newKey: v.number(), id: v.string(), value: v.number(), }), ), }, + returns: v.object({ + operations: v.object({ + inserts: v.number(), + deletes: v.number(), + updates: v.number(), + }), + }), handler: async (ctx, { inserts, deletes, updates }) => {Based on coding guidelines.
307-347: Add return validator for consistency.The function returns an object with
method,count, anddurationMsproperties but lacks a return validator.♻️ Add return validator
export const comparePerformance = mutation({ args: { count: v.number(), useBatching: v.boolean(), }, + returns: v.object({ + method: v.string(), + count: v.number(), + durationMs: v.number(), + }), handler: async (ctx, { count, useBatching }) => {Based on coding guidelines.
352-385: Add return validator for consistency.The function returns an object with
queuedandtotalInRangeproperties but lacks a return validator.♻️ Add return validator
export const autoFlushOnRead = mutation({ args: { count: v.number(), }, + returns: v.object({ + queued: v.number(), + totalInRange: v.number(), + }), handler: async (ctx, { count }) => {Based on coding guidelines.
394-439: Add return validator for consistency.The function returns an object with
operations,namespaces, andmessageproperties but lacks a return validator.♻️ Add return validator
export const batchedWritesWithNamespaces = mutation({ args: { operations: v.array( v.object({ namespace: v.string(), key: v.number(), id: v.string(), value: v.number(), }), ), }, + returns: v.object({ + operations: v.number(), + namespaces: v.number(), + message: v.string(), + }), handler: async (ctx, { operations }) => {Based on coding guidelines.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
CHANGELOG.mdexample/convex/batchedWrites.tspackage.json
✅ Files skipped from review due to trivial changes (1)
- package.json
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/batchedWrites.ts
🧠 Learnings (17)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas
Applied to files:
example/convex/batchedWrites.ts
🧬 Code graph analysis (1)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (3)
DirectAggregate(738-856)TableAggregate(891-1042)count(159-172)
🔇 Additional comments (4)
CHANGELOG.md (1)
3-5: LGTM! Clear documentation of the buffering feature.The changelog entry accurately describes the new buffering writes feature and its performance benefit when using triggers.
example/convex/batchedWrites.ts (3)
1-18: LGTM! Excellent documentation and proper imports.The file header clearly explains the buffering pattern and its benefits, especially for trigger-based workflows. Imports are appropriate for the demonstrated functionality.
20-33: LGTM! Proper aggregate configuration.Both DirectAggregate and TableAggregate instances are correctly configured. The use of negative score for descending order in the leaderboard is a proper pattern.
94-120: LGTM! Excellent pattern for trigger-based buffering.The custom mutation wrapper correctly implements the buffering lifecycle: enabling before the mutation runs, wrapping the database with triggers, and flushing only on success. This is a robust pattern that prevents partial flushes on errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/component/public.ts (1)
140-150: Missing return validator onreplaceOrInsertmutation.Similar to
deleteIfExists, this mutation is missingreturns: v.null().Proposed fix
export const replaceOrInsert = mutation({ args: { currentKey: v.any(), newKey: v.any(), value: v.any(), summand: v.optional(v.number()), namespace: v.optional(v.any()), newNamespace: v.optional(v.any()), }, + returns: v.null(), handler: replaceOrInsertHandler, });
🤖 Fix all issues with AI agents
In @src/client/index.ts:
- Around line 147-154: The currentFlushPromise is left set if
ctx.runMutation(this.component.public.batch, { operations }) rejects; change the
pattern in the flush logic to assign a local promise from ctx.runMutation(...)
and attach a .finally() handler that sets this.currentFlushPromise = null so the
state is always cleared regardless of success or failure; keep awaiting the
assigned promise (await localPromise) and reference unique symbols:
currentFlushPromise, ctx.runMutation, this.component.public.batch, and
operations when locating the code to update.
In @src/component/public.ts:
- Around line 120-123: The deleteIfExists mutation is missing a return
validator; update the export const deleteIfExists = mutation({ ... })
declaration to include a returns: v.null() validator alongside args and handler
(i.e., add returns: v.null() to the mutation options for deleteIfExists) so the
mutation explicitly validates its null return value.
🧹 Nitpick comments (3)
src/component/public.ts (2)
274-291: Consider adding a type-safe sentinel or documenting the collision risk.The sentinel value
"__undefined__"for undefined namespaces could theoretically collide with a namespace that serializes to exactly"__undefined__"(e.g., the literal string"__undefined__"). While unlikely in practice, consider using aSymbolor a more unique sentinel pattern.Optional: Use Symbol for sentinel
+const UNDEFINED_NAMESPACE_KEY = Symbol("undefined_namespace"); + // Helper function to get or create tree for a namespace const getTreeForNamespace = async (namespace: any) => { - // Use a sentinel value for undefined namespace since JSON.stringify(undefined) returns undefined - const key = - namespace === undefined - ? "__undefined__" - : JSON.stringify(convexToJson(namespace)); + const key: string | symbol = + namespace === undefined + ? UNDEFINED_NAMESPACE_KEY + : JSON.stringify(convexToJson(namespace)); if (!treesMap.has(key)) {Note: This would require changing
treesMaptoMap<string | symbol, ...>.
294-321: Consider adding exhaustive type checking for operation routing.The if/else chain handles all current operation types, but adding an exhaustive check would catch future additions at compile time and runtime.
Optional: Add exhaustive check
} else if (op.type === "replaceOrInsert") { const { type: _, ...args } = op; // Handle delete from original namespace const deleteTree = await getTreeForNamespace(op.namespace); const newTree = await getTreeForNamespace(op.newNamespace); await replaceOrInsertHandler(ctx, args, deleteTree, newTree); + } else { + const _exhaustiveCheck: never = op; + throw new Error(`Unknown operation type: ${(op as any).type}`); } }example/convex/batchedWrites.ts (1)
311-352: Consider using unique keys to avoid collision on repeated runs.The hardcoded key offset
1000000and id patternperf-test-${i}will cause collisions if this mutation is run multiple times without clearing the aggregate first. Consider using a timestamp or random prefix for truly independent runs.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
example/convex/batchedWrites.tssrc/client/index.tssrc/component/public.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/batchedWrites.ts
🧠 Learnings (28)
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if ctx.runMutation throws an error, the entire mutation is rolled back, including any in-memory state changes and queued operations. Do not implement or rely on manual queue-preservation or compensating logic in response to a failed runMutation, as it would undermine the transactional guarantees. When reviewing TypeScript files under src that use Convex mutations, assume that no explicit preservation of pre-call queues is needed for rollback scenarios; focus on correctness of the mutation call itself and downstream effects, not on attempting to manually undo queued work.
Applied to files:
src/client/index.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
src/client/index.tsexample/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: Convex runs in a deterministic execution environment without transient failures (no network flakes, etc.). Failures in `ctx.runMutation` only occur due to logical bugs, resource limits (reading too much data), or deterministic errors, so retrying the same operations has near-zero likelihood of succeeding.
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
src/client/index.tsexample/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
src/client/index.tsexample/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Applied to files:
src/client/index.tsexample/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the database
Applied to files:
src/client/index.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
src/client/index.tsexample/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runAction` to call an action from another action; otherwise pull out shared code into a helper async function
Applied to files:
src/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
src/client/index.tsexample/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
example/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
example/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
example/convex/batchedWrites.tssrc/component/public.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if `ctx.runMutation` throws, the entire mutation rolls back, including any in-memory state changes made before the call. This transactional guarantee means that operations queued before a failed `ctx.runMutation` call will be automatically restored, so explicit error handling to preserve queues is unnecessary and would break transactional semantics.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the aggregate repository, return validators are only required for Convex functions in the `src/component` directory, not in `example/` or `convex/` directories.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.int64()` instead of deprecated `v.bigint()` for representing signed 64-bit integers
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex
Applied to files:
src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex storage stores items as `Blob` objects; convert all items to/from a `Blob` when using Convex storage
Applied to files:
src/component/public.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the get-convex/aggregate repository, return validators are required only for Convex functions located in the src/component directory. Do not require validators for files under example/ or convex/, and limit this rule to TypeScript files under src/component (exclude other paths) indicating that only those files should be governed by the validator policy.
Applied to files:
src/component/public.ts
🧬 Code graph analysis (2)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (3)
DirectAggregate(736-854)TableAggregate(889-1040)count(175-188)
src/component/public.ts (1)
src/component/btree.ts (7)
Key(27-27)Value(28-28)Namespace(29-29)deleteHandler(83-113)insertHandler(45-81)getOrCreateTree(949-985)DEFAULT_MAX_NODE_SIZE(25-25)
🔇 Additional comments (12)
src/component/public.ts (2)
1-14: LGTM - Clean import organization.The new imports for
convexToJson,Value,DatabaseWriter,Key,Namespace, andDoctypes are correctly added to support the new handler functions and batch mutation.
87-118: LGTM - Well-structured handler extraction.The
replaceHandlercorrectly separates delete (from original namespace) and insert (to new namespace) operations, with proper tree injection support for batching.src/client/index.ts (5)
39-74: LGTM - Well-typed buffered operation union.The
BufferedOperationtype correctly mirrors the operation types accepted by the server-sidebatchmutation, ensuring type consistency between client buffering and server processing.
94-96: LGTM - Instance-level buffering state.The buffering state is correctly scoped to the
Aggregateinstance, allowing controlled buffering within a mutation context.
156-168: LGTM - Proper read consistency handling.The
flushBeforeReadmethod correctly ensures buffered writes are flushed before reads, with a helpful error message when called from a query context.
488-503: LGTM - Consistent buffering pattern in write operations.The
_insertmethod (and other write methods following the same pattern) correctly queues operations when buffering is active and awaits any in-progress flush when not buffering.
175-188: LGTM - Read operations properly flush before reading.The
countmethod and other read operations correctly callflushBeforeReadto ensure consistency with buffered writes.example/convex/batchedWrites.ts (5)
1-23: LGTM - Well-documented example setup.The file header and aggregate setup are clear. The
DirectAggregateis correctly instantiated with thebatchedWritescomponent.
94-120: LGTM - Excellent pattern for combining triggers with buffering.The
customMutationwrapper correctly:
- Enables buffering before operations
- Wraps the database with triggers
- Flushes buffered operations only on success
This is the recommended pattern for trigger-based aggregates.
45-77: LGTM - Clear basic example.This example effectively demonstrates the fundamental buffering pattern:
startBuffering→ queue operations →finishBuffering→ verify results.
128-160: LGTM - Effective trigger + buffering demonstration.The
addMultipleScoresmutation clearly shows how table inserts trigger automatic aggregate updates, and how buffering batches all those updates together.
392-444: LGTM - Good namespace grouping example.The
batchedWritesWithNamespacesmutation effectively demonstrates how operations across multiple namespaces are automatically grouped and processed efficiently.
| this.currentFlushPromise = ctx | ||
| .runMutation(this.component.public.batch, { | ||
| operations, | ||
| }) | ||
| .then(() => (this.currentFlushPromise = null)); | ||
| // end critical section | ||
| await this.currentFlushPromise; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issue: currentFlushPromise not cleared on mutation failure.
If ctx.runMutation throws, the .then() callback won't execute, leaving currentFlushPromise set. Subsequent calls will wait indefinitely on the rejected promise (which actually resolves/rejects, so it won't hang, but the state is inconsistent). Consider using .finally() to ensure cleanup.
Proposed fix
this.currentFlushPromise = ctx
.runMutation(this.component.public.batch, {
operations,
})
- .then(() => (this.currentFlushPromise = null));
+ .finally(() => (this.currentFlushPromise = null));
// end critical section
await this.currentFlushPromise;🤖 Prompt for AI Agents
In @src/client/index.ts around lines 147 - 154, The currentFlushPromise is left
set if ctx.runMutation(this.component.public.batch, { operations }) rejects;
change the pattern in the flush logic to assign a local promise from
ctx.runMutation(...) and attach a .finally() handler that sets
this.currentFlushPromise = null so the state is always cleared regardless of
success or failure; keep awaiting the assigned promise (await localPromise) and
reference unique symbols: currentFlushPromise, ctx.runMutation,
this.component.public.batch, and operations when locating the code to update.
| export const deleteIfExists = mutation({ | ||
| args: { key: v.any(), namespace: v.optional(v.any()) }, | ||
| handler: async (ctx, { key, namespace }) => { | ||
| try { | ||
| await deleteHandler(ctx, { key, namespace }); | ||
| } catch (e) { | ||
| if (e instanceof ConvexError && e.data?.code === "DELETE_MISSING_KEY") { | ||
| return; | ||
| } | ||
| throw e; | ||
| } | ||
| }, | ||
| handler: deleteIfExistsHandler, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing return validator on deleteIfExists mutation.
Per Convex best practices, all mutations should include a return validator. This mutation is missing returns: v.null().
Proposed fix
export const deleteIfExists = mutation({
args: { key: v.any(), namespace: v.optional(v.any()) },
+ returns: v.null(),
handler: deleteIfExistsHandler,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const deleteIfExists = mutation({ | |
| args: { key: v.any(), namespace: v.optional(v.any()) }, | |
| handler: async (ctx, { key, namespace }) => { | |
| try { | |
| await deleteHandler(ctx, { key, namespace }); | |
| } catch (e) { | |
| if (e instanceof ConvexError && e.data?.code === "DELETE_MISSING_KEY") { | |
| return; | |
| } | |
| throw e; | |
| } | |
| }, | |
| handler: deleteIfExistsHandler, | |
| }); | |
| export const deleteIfExists = mutation({ | |
| args: { key: v.any(), namespace: v.optional(v.any()) }, | |
| returns: v.null(), | |
| handler: deleteIfExistsHandler, | |
| }); |
🤖 Prompt for AI Agents
In @src/component/public.ts around lines 120 - 123, The deleteIfExists mutation
is missing a return validator; update the export const deleteIfExists =
mutation({ ... }) declaration to include a returns: v.null() validator alongside
args and handler (i.e., add returns: v.null() to the mutation options for
deleteIfExists) so the mutation explicitly validates its null return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
example/convex/batchedWrites.ts (2)
187-236: Good performance comparison implementation.The mutation correctly demonstrates batching vs. non-batching performance. The use of
customCtxfor database operations (lines 205, 219) andctxfor aggregate operations (lines 197, 213) is correct.Optional improvement: Consider adding a label to
console.time()on Line 198 for better log clarity, e.g.,console.time("compare-overall").📊 Optional: Add label for clarity
- console.time(); + console.time("compare-overall"); if (useBatching) {- console.timeEnd(); + console.timeEnd("compare-overall");
392-444: Good namespace batching example.The mutation demonstrates how operations across multiple namespaces are automatically grouped and optimized in a single batch call.
Optional improvement: Consider moving the
namespacedAggregateinstance (lines 412-416) to module level alongside the other aggregates. This avoids creating a new instance on each invocation. However, since this is an example file demonstrating the pattern, the current approach is acceptable.♻️ Optional: Move to module level
At the top of the file, after the
leaderboardAggregatedefinition:const namespacedAggregate = new DirectAggregate<{ Key: number; Id: string; Namespace: string; }>(components.batchedWrites);Then in the handler, remove lines 412-416 and use the module-level instance directly.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
example/convex/batchedWrites.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}
📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)
example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions withquery,mutation,actiondecorators andhandlerproperty
Use array validators withv.array()to specify element types in Convex function arguments and schemas
Usev.null()validator when returning or defining fields that can be null values in Convex functions
Usev.int64()instead of deprecatedv.bigint()for representing signed 64-bit integers
Usev.record()for defining record types;v.map()andv.set()are not supported in Convex
ALWAYS include argument and return validators for all Convex functions includingquery,internalQuery,mutation,internalMutation,action, andinternalAction; usereturns: v.null()if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
UseId<'tableName'>TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Useas constfor string literals in discriminated union types in TypeScript
Always define arrays asconst array: Array<T> = [...]with explicit type annotation
Always define records asconst record: Record<KeyType, ValueType> = {...}with explicit type annotation
UseinternalQuery,internalMutation, andinternalActionto register private functions that are only callable by other Convex functions
Usequery,mutation, andactionto register public functions exposed to the API; do NOT use these for sensitive internal functions
Usectx.runQueryto call a query from a query, mutation, or action
Usectx.runMutationto call a mutation from a mutation or action
Usectx.runActionto call an action from another action; otherwise pull out shared code into a helper async function
When usingctx.runQuery,ctx.runMutation, orctx.runActionto call a function in the same file, specify a type annotation on the return value
Organize files with publi...
Files:
example/convex/batchedWrites.ts
🧠 Learnings (20)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if `ctx.runMutation` throws, the entire mutation rolls back, including any in-memory state changes made before the call. This transactional guarantee means that operations queued before a failed `ctx.runMutation` call will be automatically restored, so explicit error handling to preserve queues is unnecessary and would break transactional semantics.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: Convex runs in a deterministic execution environment without transient failures (no network flakes, etc.). Failures in `ctx.runMutation` only occur due to logical bugs, resource limits (reading too much data), or deterministic errors, so retrying the same operations has near-zero likelihood of succeeding.
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas
Applied to files:
example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the aggregate repository, return validators are only required for Convex functions in the `src/component` directory, not in `example/` or `convex/` directories.
Applied to files:
example/convex/batchedWrites.ts
🧬 Code graph analysis (1)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
aggregate(25-28)src/client/index.ts (3)
DirectAggregate(736-854)TableAggregate(889-1040)count(175-188)
🔇 Additional comments (9)
example/convex/batchedWrites.ts (9)
1-18: Excellent documentation and setup!The file header clearly explains the batching concept and its benefits, especially for trigger-based workflows. The imports follow Convex conventions correctly.
20-40: LGTM! Clean setup of aggregate instances.The module-level aggregate instances follow the correct pattern. Using negative score for descending leaderboard order on Line 31 is an elegant solution. The
resetmutation correctly usesinternalMutationfor internal-only access.
42-77: Excellent demonstration of basic batching pattern!The mutation correctly pairs
startBuffering()withfinishBuffering()and includes validation to ensure correctness. The pattern of queuing operations during buffering and then flushing is implemented correctly.
79-120: Excellent pattern for combining triggers with batching!The
customMutationwrapper correctly implements the recommended pattern: enabling buffering before the handler runs and flushing on success. The lack of anonFailurehandler is correct because Convex's transactional rollback automatically restores the buffering state.
122-160: Great example of trigger-based batching!The mutation demonstrates how triggers automatically update the aggregate while batching consolidates all updates into a single component call. The validation logic (lines 148-153) ensures the sum values are correctly maintained.
162-185: LGTM! Proper use of ctx.db.patch.The mutation correctly demonstrates batched replace operations via triggers. Using
ctx.db.patchon Line 177 is the appropriate Convex API for updating existing documents.
238-306: Excellent demonstration of mixed operation batching!This mutation showcases how different operation types (inserts, deletes, updates) can be efficiently batched together. The use of idempotent variants (
deleteIfExistson Line 280,replaceOrInserton Line 288) is appropriate for this pattern.
308-352: Clean performance comparison!The mutation provides a straightforward batching vs. unbatched comparison. Using high key values (1000000+) on lines 325 and 338 is a good practice to avoid conflicts with other examples in the file.
354-390: Great demonstration of auto-flush behavior!This mutation effectively shows how read operations automatically flush pending writes before executing. The comment on lines 374-375 clearly explains this behavior, making it educational for users of the library.
Allow batching pending writes to reduce the amount of calls to the component during mutations that may be using triggers
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Summary by CodeRabbit
New Features
Documentation
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.