Skip to content

Commit a8505c9

Browse files
committed
update example and change buffering to startBuffering / finishBuffering
1 parent 2c05f07 commit a8505c9

2 files changed

Lines changed: 161 additions & 61 deletions

File tree

example/convex/batchedWrites.ts

Lines changed: 146 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,34 @@
44
* This demonstrates how to use the `.buffer()` method to queue write
55
* operations and flush them in a batch, reducing the number of mutations
66
* and improving performance.
7+
*
8+
* This is especially useful when using triggers, as it batches all triggered
9+
* aggregate writes into a single component call instead of one per write.
710
*/
811

9-
import { DirectAggregate } from "@convex-dev/aggregate";
12+
import { DirectAggregate, TableAggregate } from "@convex-dev/aggregate";
1013
import { mutation } from "./_generated/server";
1114
import { components } from "./_generated/api.js";
1215
import { v } from "convex/values";
1316
import { customMutation } from "convex-helpers/server/customFunctions";
17+
import { Triggers } from "convex-helpers/server/triggers";
18+
import type { DataModel } from "./_generated/dataModel";
1419

1520
const aggregate = new DirectAggregate<{
1621
Key: number;
1722
Id: string;
1823
}>(components.batchedWrites);
1924

25+
// Example with TableAggregate and Triggers
26+
const leaderboardAggregate = new TableAggregate<{
27+
Key: number;
28+
DataModel: DataModel;
29+
TableName: "leaderboard";
30+
}>(components.batchedWrites, {
31+
sortKey: (doc) => -doc.score, // Negative for descending order
32+
sumValue: (doc) => doc.score,
33+
});
34+
2035
/**
2136
* Basic example: Enable buffering, queue operations, then flush manually.
2237
*/
@@ -26,7 +41,7 @@ export const basicBatchedWrites = mutation({
2641
},
2742
handler: async (ctx, { count }) => {
2843
// Enable buffering mode - modifies the aggregate instance in place
29-
aggregate.buffer(true);
44+
aggregate.startBuffering();
3045

3146
// Queue multiple insert operations
3247
for (let i = 0; i < count; i++) {
@@ -38,9 +53,7 @@ export const basicBatchedWrites = mutation({
3853
}
3954

4055
// Disable buffering after we're done
41-
aggregate.buffer(false);
42-
// Flush all buffered operations in a single batch
43-
await aggregate.flush(ctx);
56+
aggregate.finishBuffering(ctx);
4457

4558
// Read operations work normally (and auto-flush if needed)
4659
const total = await aggregate.count(ctx);
@@ -50,58 +63,146 @@ export const basicBatchedWrites = mutation({
5063
});
5164

5265
/**
53-
* Advanced example: Use custom functions with onSuccess callback.
66+
* Advanced example: Use custom functions with Triggers and buffering.
67+
*
68+
* This is the RECOMMENDED pattern when using triggers!
5469
*
55-
* This pattern is useful when you are also using triggers, to avoid all
56-
* triggered writes calling the component individually.
70+
* When using triggers, each table write triggers an aggregate write.
71+
* If you insert 100 rows, that's 100 separate calls to the aggregate component.
72+
* With buffering, all 100 writes are batched into a single component call.
73+
*
74+
* Performance benefits:
75+
* - Single component call instead of N calls
76+
* - Single tree fetch instead of N fetches
77+
* - Better handling of write contention
5778
*/
5879

59-
// Create a custom mutation that uses buffered aggregate
60-
const mutationWithBuffering = customMutation(mutation, {
80+
// Set up triggers
81+
const triggers = new Triggers<DataModel>();
82+
triggers.register("leaderboard", leaderboardAggregate.trigger());
83+
84+
// Create a custom mutation that:
85+
// 1. Wraps the database with triggers
86+
// 2. Enables buffering before the mutation runs
87+
// 3. Flushes after the mutation completes successfully
88+
const mutationWithTriggers = customMutation(mutation, {
6189
args: {},
62-
input: async () => {
63-
aggregate.buffer(true);
90+
input: async (ctx) => {
91+
// Enable buffering for all aggregate operations
92+
leaderboardAggregate.startBuffering();
93+
6494
return {
65-
ctx: {},
95+
ctx: {
96+
// Wrap db with triggers
97+
...triggers.wrapDB(ctx),
98+
},
6699
args: {},
67100
onSuccess: async ({ ctx }) => {
68-
await aggregate.flush(ctx);
101+
// Flush all buffered operations in a single batch
102+
await leaderboardAggregate.finishBuffering(ctx);
69103
},
70104
};
71105
},
72106
});
73107

74108
/**
75-
* Example using custom function with onSuccess callback.
109+
* Example: Add multiple scores with triggers and batching.
76110
*
77-
* This demonstrates the recommended pattern for batching:
78-
* - Enable buffering at the start (in customCtx)
79-
* - Queue operations throughout the function using the global aggregate
80-
* - Flush in the onSuccess callback
111+
* Without buffering: Each insert triggers a separate aggregate.insert call
112+
* With buffering: All inserts are batched into one aggregate.batch call
81113
*/
82-
export const batchedWritesWithOnSuccess = mutationWithBuffering({
114+
export const addMultipleScores = mutationWithTriggers({
83115
args: {
84-
items: v.array(
116+
scores: v.array(
85117
v.object({
86-
key: v.number(),
87-
id: v.string(),
88-
value: v.number(),
118+
name: v.string(),
119+
score: v.number(),
89120
}),
90121
),
91122
},
92-
handler: async (ctx, { items }) => {
93-
// Queue all operations - they're stored in memory, not sent yet
94-
// We use the global 'aggregate' instance which has buffering enabled
95-
for (const item of items) {
96-
await aggregate.insert(ctx, {
97-
key: item.key,
98-
id: item.id,
99-
sumValue: item.value,
100-
});
123+
handler: async (ctx, { scores }) => {
124+
// Just insert into the table - the trigger automatically
125+
// updates the aggregate, and buffering batches all the updates
126+
for (const { name, score } of scores) {
127+
await ctx.db.insert("leaderboard", { name, score });
101128
}
102129

103130
return {
104-
queued: items.length,
131+
inserted: scores.length,
132+
message: `Added ${scores.length} scores with batched aggregate updates`,
133+
};
134+
},
135+
});
136+
137+
/**
138+
* Example: Update multiple scores - shows replace operations are also batched
139+
*/
140+
export const updateMultipleScores = mutationWithTriggers({
141+
args: {
142+
updates: v.array(
143+
v.object({
144+
id: v.id("leaderboard"),
145+
newScore: v.number(),
146+
}),
147+
),
148+
},
149+
handler: async (ctx, { updates }) => {
150+
// Each patch triggers aggregate.replace, all batched together
151+
for (const { id, newScore } of updates) {
152+
await ctx.db.patch(id, { score: newScore });
153+
}
154+
155+
return {
156+
updated: updates.length,
157+
message: `Updated ${updates.length} scores with batched aggregate updates`,
158+
};
159+
},
160+
});
161+
162+
/**
163+
* Example showing the difference with and without batching
164+
*/
165+
export const compareTriggersWithAndWithoutBatching = mutation({
166+
args: {
167+
count: v.number(),
168+
useBatching: v.boolean(),
169+
},
170+
handler: async (ctx, { count, useBatching }) => {
171+
const start = Date.now();
172+
173+
const customCtx = triggers.wrapDB(ctx);
174+
if (useBatching) {
175+
// With batching: all aggregate operations batched into one call
176+
leaderboardAggregate.startBuffering();
177+
178+
for (let i = 0; i < count; i++) {
179+
await customCtx.db.insert("leaderboard", {
180+
name: `player-${i}`,
181+
score: Math.floor(Math.random() * 1000),
182+
});
183+
}
184+
185+
await leaderboardAggregate.finishBuffering(ctx);
186+
} else {
187+
// Without batching: each insert makes a separate aggregate call
188+
189+
for (let i = 0; i < count; i++) {
190+
await customCtx.db.insert("leaderboard", {
191+
name: `player-${i}`,
192+
score: Math.floor(Math.random() * 1000),
193+
});
194+
}
195+
}
196+
197+
const duration = Date.now() - start;
198+
199+
return {
200+
method: useBatching ? "with batching" : "without batching",
201+
count,
202+
durationMs: duration,
203+
message: useBatching
204+
? `1 batched call to aggregate component`
205+
: `${count} individual calls to aggregate component`,
105206
};
106207
},
107208
});
@@ -135,7 +236,7 @@ export const complexBatchedOperations = mutation({
135236
},
136237
handler: async (ctx, { inserts, deletes, updates }) => {
137238
// Enable buffering
138-
aggregate.buffer(true);
239+
aggregate.startBuffering();
139240

140241
// Queue inserts
141242
for (const item of inserts) {
@@ -163,11 +264,8 @@ export const complexBatchedOperations = mutation({
163264
);
164265
}
165266

166-
// Flush all operations at once
167-
await aggregate.flush(ctx);
168-
169-
// Disable buffering
170-
aggregate.buffer(false);
267+
// Flush all operations at once and stop buffering
268+
await aggregate.finishBuffering(ctx);
171269

172270
return {
173271
operations: {
@@ -192,7 +290,7 @@ export const comparePerformance = mutation({
192290

193291
if (useBatching) {
194292
// Batched approach
195-
aggregate.buffer(true);
293+
aggregate.startBuffering();
196294

197295
for (let i = 0; i < count; i++) {
198296
await aggregate.insert(ctx, {
@@ -202,8 +300,7 @@ export const comparePerformance = mutation({
202300
});
203301
}
204302

205-
await aggregate.flush(ctx);
206-
aggregate.buffer(false);
303+
await aggregate.finishBuffering(ctx);
207304
} else {
208305
// Unbatched approach
209306
for (let i = 0; i < count; i++) {
@@ -234,7 +331,7 @@ export const autoFlushOnRead = mutation({
234331
},
235332
handler: async (ctx, { count }) => {
236333
// Enable buffering
237-
aggregate.buffer(true);
334+
aggregate.startBuffering();
238335

239336
// Queue some operations
240337
for (let i = 0; i < count; i++) {
@@ -253,8 +350,8 @@ export const autoFlushOnRead = mutation({
253350
},
254351
});
255352

256-
// Disable buffering
257-
aggregate.buffer(false);
353+
// Flush all operations at once and stop buffering
354+
await aggregate.finishBuffering(ctx);
258355

259356
return {
260357
queued: count,
@@ -290,7 +387,7 @@ export const batchedWritesWithNamespaces = mutation({
290387
}>(components.batchedWrites);
291388

292389
// Enable buffering
293-
namespacedAggregate.buffer(true);
390+
namespacedAggregate.startBuffering();
294391

295392
// Queue operations - they'll be grouped by namespace internally
296393
for (const op of operations) {
@@ -302,12 +399,9 @@ export const batchedWritesWithNamespaces = mutation({
302399
});
303400
}
304401

305-
// Flush all operations
402+
// Flush all operations and stop buffering
306403
// The batch mutation will group by namespace automatically
307-
await namespacedAggregate.flush(ctx);
308-
309-
// Disable buffering
310-
namespacedAggregate.buffer(false);
404+
await namespacedAggregate.finishBuffering(ctx);
311405

312406
// Count unique namespaces
313407
const namespaces = new Set(operations.map((op) => op.namespace));

src/client/index.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,23 +104,29 @@ export class Aggregate<
104104
constructor(protected component: ComponentApi) {}
105105

106106
/**
107-
* Enable or disable buffering mode. When buffering is enabled, write operations are
108-
* queued and sent in a batch when flush() is called or when any read
107+
* Start buffering write operations. When buffering is enabled, write operations are
108+
* queued and sent in a batch when flush() or stopBuffering() is called or when any read
109109
* operation is performed.
110110
*
111-
* Modifies this instance in place and returns it for chaining.
112-
*
113111
* Example usage:
114112
* ```ts
115-
* aggregate.buffer(true);
113+
* aggregate.startBuffering();
116114
* aggregate.insert(ctx, { key: 1, id: "a" });
117115
* aggregate.insert(ctx, { key: 2, id: "b" });
118-
* await aggregate.flush(ctx); // Send all buffered operations
116+
* await aggregate.stopBuffering(ctx); // Send all buffered operations
119117
* ```
120118
*/
121-
buffer(enabled: boolean): this {
122-
this.isBuffering = enabled;
123-
return this;
119+
startBuffering(): void {
120+
this.isBuffering = true;
121+
}
122+
123+
/**
124+
* Stop buffering write operations and flush all buffered operations.
125+
* @param ctx - The mutation context, used to flush the buffered operations.
126+
*/
127+
async finishBuffering(ctx: RunMutationCtx): Promise<void> {
128+
this.isBuffering = false;
129+
await this.flush(ctx);
124130
}
125131

126132
/**

0 commit comments

Comments
 (0)