Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.2.2-alpha.0

- Enables buffering writes for performance when using triggers

## 0.2.1

- Allow passing { bounds: { eq: key }}, supporting non-array keys for counts
Expand Down
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,74 @@ export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
The [`example/convex/photos.ts`](example/convex/photos.ts) example uses a
trigger.

### 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.

Comment on lines +492 to +559
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

### Repair incorrect aggregates

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

## Performance Optimizations

### Batch Operations
### Batch Read Operations

For improved performance when making multiple similar queries, the Aggregate
component provides batch versions of common operations:
component provides batch versions of common read operations:

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

### Batch Write Operations

When making multiple write operations (inserts, deletes, or updates), you can
use the batching API to queue operations and send them in a single call to the
aggregate component. This is especially valuable when using triggers.

**When to use batching:**

- Making multiple aggregate writes in a single mutation
- Using triggers that automatically update aggregates on table changes
- Bulk operations like importing data or backfilling

The batching API works by enabling buffering mode, queueing operations in
memory, and flushing them all at once:

```ts
// Enable buffering
aggregate.startBuffering();

// Queue operations (not sent yet)
await aggregate.insert(ctx, { key: 1, id: "a" });
await aggregate.insert(ctx, { key: 2, id: "b" });
await aggregate.insert(ctx, { key: 3, id: "c" });

// Flush all operations in a single batch and stop buffering
await aggregate.finishBuffering(ctx);
```

**Benefits of batching:**

- **Single component call** - One mutation instead of N separate calls
- **Single tree fetch** - The B-tree is fetched once for all operations
- **Better write contention** - All operations processed atomically together
- **Reduced overhead** - Less network and serialization overhead

See the [Optimizing Triggers with Batching](#optimizing-triggers-with-batching)
section for the recommended pattern when using triggers, and see
[`example/convex/batchedWrites.ts`](example/convex/batchedWrites.ts) for
complete examples.

## Reactivity and Atomicity

Like all Convex queries, aggregates are
Expand Down
3 changes: 3 additions & 0 deletions example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @module
*/

import type * as batchedWrites from "../batchedWrites.js";
import type * as btree from "../btree.js";
import type * as crons from "../crons.js";
import type * as leaderboard from "../leaderboard.js";
Expand All @@ -23,6 +24,7 @@ import type {
} from "convex/server";

declare const fullApi: ApiFromModules<{
batchedWrites: typeof batchedWrites;
btree: typeof btree;
crons: typeof crons;
leaderboard: typeof leaderboard;
Expand Down Expand Up @@ -65,5 +67,6 @@ export declare const components: {
photos: import("@convex-dev/aggregate/_generated/component.js").ComponentApi<"photos">;
stats: import("@convex-dev/aggregate/_generated/component.js").ComponentApi<"stats">;
btreeAggregate: import("@convex-dev/aggregate/_generated/component.js").ComponentApi<"btreeAggregate">;
batchedWrites: import("@convex-dev/aggregate/_generated/component.js").ComponentApi<"batchedWrites">;
migrations: import("@convex-dev/migrations/_generated/component.js").ComponentApi<"migrations">;
};
Loading
Loading