Skip to content

Commit a40535e

Browse files
committed
readme
1 parent a8505c9 commit a40535e

1 file changed

Lines changed: 110 additions & 2 deletions

File tree

README.md

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,74 @@ export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
489489
The [`example/convex/photos.ts`](example/convex/photos.ts) example uses a
490490
trigger.
491491

492+
### Optimizing Triggers with Batching
493+
494+
**Recommended:** When using triggers, combine them with the batching API for
495+
optimal performance.
496+
497+
Without batching, each table write triggers a separate call to the aggregate
498+
component. If you insert 100 rows in a mutation, that's 100 individual calls to
499+
the aggregate component, each fetching and updating the B-tree separately.
500+
501+
With batching, all triggered aggregate operations are queued and sent as a
502+
single batch at the end of the mutation. This provides:
503+
504+
- **Single component call** instead of N separate calls
505+
- **Single tree fetch** instead of N fetches
506+
- **Better write contention handling** - one atomic update instead of many
507+
- **Significant performance improvement** - especially for bulk operations
508+
509+
Here's how to set it up:
510+
511+
```ts
512+
import { TableAggregate } from "@convex-dev/aggregate";
513+
import { Triggers } from "convex-helpers/server/triggers";
514+
import { customMutation } from "convex-helpers/server/customFunctions";
515+
import { mutation as rawMutation } from "./_generated/server";
516+
517+
const aggregate = new TableAggregate<{
518+
Key: number;
519+
DataModel: DataModel;
520+
TableName: "leaderboard";
521+
}>(components.aggregate, {
522+
sortKey: (doc) => -doc.score,
523+
});
524+
525+
// Set up triggers
526+
const triggers = new Triggers<DataModel>();
527+
triggers.register("leaderboard", aggregate.trigger());
528+
529+
// Create a custom mutation that enables buffering and flushes on success
530+
const mutation = customMutation(rawMutation, {
531+
args: {},
532+
input: async (ctx) => {
533+
aggregate.startBuffering();
534+
return {
535+
ctx: triggers.wrapDB(ctx),
536+
args: {},
537+
onSuccess: async ({ ctx }) => {
538+
await aggregate.finishBuffering(ctx);
539+
},
540+
};
541+
},
542+
});
543+
544+
// Now use this mutation in your functions
545+
export const addScores = mutation({
546+
args: { scores: v.array(v.object({ name: v.string(), score: v.number() })) },
547+
handler: async (ctx, { scores }) => {
548+
// Each insert triggers an aggregate operation, but they're all batched!
549+
for (const { name, score } of scores) {
550+
await ctx.db.insert("leaderboard", { name, score });
551+
}
552+
// The flush happens automatically in the onSuccess callback
553+
},
554+
});
555+
```
556+
557+
See [`example/convex/batchedWrites.ts`](example/convex/batchedWrites.ts) for a
558+
complete working example with performance comparisons.
559+
492560
### Repair incorrect aggregates
493561

494562
If some mutation or direct write in the Dashboard updated the source of truth
@@ -507,10 +575,10 @@ aggregates based on the diff of these two paginated data streams.
507575

508576
## Performance Optimizations
509577

510-
### Batch Operations
578+
### Batch Read Operations
511579

512580
For improved performance when making multiple similar queries, the Aggregate
513-
component provides batch versions of common operations:
581+
component provides batch versions of common read operations:
514582

515583
- `countBatch()` - Count items for multiple bounds in a single call
516584
- `sumBatch()` - Sum items for multiple bounds in a single call
@@ -547,6 +615,46 @@ The batch functions accept arrays of query parameters and return arrays of
547615
results in the same order, making them drop-in replacements for multiple
548616
individual calls while providing better performance characteristics.
549617

618+
### Batch Write Operations
619+
620+
When making multiple write operations (inserts, deletes, or updates), you can
621+
use the batching API to queue operations and send them in a single call to the
622+
aggregate component. This is especially valuable when using triggers.
623+
624+
**When to use batching:**
625+
626+
- Making multiple aggregate writes in a single mutation
627+
- Using triggers that automatically update aggregates on table changes
628+
- Bulk operations like importing data or backfilling
629+
630+
The batching API works by enabling buffering mode, queueing operations in
631+
memory, and flushing them all at once:
632+
633+
```ts
634+
// Enable buffering
635+
aggregate.startBuffering();
636+
637+
// Queue operations (not sent yet)
638+
await aggregate.insert(ctx, { key: 1, id: "a" });
639+
await aggregate.insert(ctx, { key: 2, id: "b" });
640+
await aggregate.insert(ctx, { key: 3, id: "c" });
641+
642+
// Flush all operations in a single batch and stop buffering
643+
await aggregate.finishBuffering(ctx);
644+
```
645+
646+
**Benefits of batching:**
647+
648+
- **Single component call** - One mutation instead of N separate calls
649+
- **Single tree fetch** - The B-tree is fetched once for all operations
650+
- **Better write contention** - All operations processed atomically together
651+
- **Reduced overhead** - Less network and serialization overhead
652+
653+
See the [Optimizing Triggers with Batching](#optimizing-triggers-with-batching)
654+
section for the recommended pattern when using triggers, and see
655+
[`example/convex/batchedWrites.ts`](example/convex/batchedWrites.ts) for
656+
complete examples.
657+
550658
## Reactivity and Atomicity
551659

552660
Like all Convex queries, aggregates are

0 commit comments

Comments
 (0)