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" ;
1013import { mutation } from "./_generated/server" ;
1114import { components } from "./_generated/api.js" ;
1215import { v } from "convex/values" ;
1316import { customMutation } from "convex-helpers/server/customFunctions" ;
17+ import { Triggers } from "convex-helpers/server/triggers" ;
18+ import type { DataModel } from "./_generated/dataModel" ;
1419
1520const 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 ) ) ;
0 commit comments