@@ -2943,5 +2943,153 @@ describe(`QueryCollection`, () => {
29432943 expect ( collection . has ( `3` ) ) . toBe ( false )
29442944 expect ( collection . has ( `4` ) ) . toBe ( false )
29452945 } )
2946+
2947+ it ( `should deduplicate queries and handle GC correctly when queries are ordered and have a LIMIT` , async ( ) => {
2948+ const baseQueryKey = [ `deduplication-gc-test` ]
2949+
2950+ // Mock queryFn to return different data based on predicates
2951+ const queryFn = vi . fn ( ) . mockImplementation ( ( context ) => {
2952+ const { meta } = context
2953+ const loadSubsetOptions = meta ?. loadSubsetOptions ?? { }
2954+ const { where, limit } = loadSubsetOptions
2955+
2956+ // Query 1: all items with category A (no limit)
2957+ if (
2958+ where ?. name === `eq` &&
2959+ where ?. args [ 0 ] . path ?. [ 0 ] === `category` &&
2960+ where ?. args [ 1 ] . value === `A` &&
2961+ ! limit
2962+ ) {
2963+ return Promise . resolve ( [
2964+ { id : `1` , name : `Item 1` , category : `A` } ,
2965+ { id : `2` , name : `Item 2` , category : `A` } ,
2966+ { id : `3` , name : `Item 3` , category : `A` } ,
2967+ ] )
2968+ }
2969+
2970+ return Promise . resolve ( [ ] )
2971+ } )
2972+
2973+ const config : QueryCollectionConfig < CategorisedItem > = {
2974+ id : `deduplication-test` ,
2975+ queryClient,
2976+ queryKey : ( ctx ) => {
2977+ const key = [ ...baseQueryKey ]
2978+ if ( ctx . where ) {
2979+ key . push ( `where` , JSON . stringify ( ctx . where ) )
2980+ }
2981+ if ( ctx . limit ) {
2982+ key . push ( `limit` , ctx . limit . toString ( ) )
2983+ }
2984+ if ( ctx . orderBy ) {
2985+ key . push ( `orderBy` , JSON . stringify ( ctx . orderBy ) )
2986+ }
2987+ return key
2988+ } ,
2989+ queryFn,
2990+ getKey,
2991+ startSync : true ,
2992+ syncMode : `on-demand` ,
2993+ }
2994+
2995+ const options = queryCollectionOptions ( config )
2996+ const collection = createCollection ( options )
2997+
2998+ // Collection should start empty with on-demand sync mode
2999+ expect ( collection . size ) . toBe ( 0 )
3000+
3001+ // Execute first query: load all rows that belong to category A (returns 3 rows)
3002+ const whereClause1 = new Func ( `eq` , [
3003+ new PropRef ( [ `category` ] ) ,
3004+ new Value ( `A` ) ,
3005+ ] )
3006+ await collection . _sync . loadSubset ( {
3007+ where : whereClause1 ,
3008+ } )
3009+
3010+ // Wait for first query data to load
3011+ await vi . waitFor ( ( ) => {
3012+ expect ( collection . size ) . toBe ( 3 )
3013+ expect ( queryFn ) . toHaveBeenCalledTimes ( 1 )
3014+ } )
3015+
3016+ // Verify all 3 items are present
3017+ expect ( collection . has ( `1` ) ) . toBe ( true )
3018+ expect ( collection . has ( `2` ) ) . toBe ( true )
3019+ expect ( collection . has ( `3` ) ) . toBe ( true )
3020+
3021+ // Execute second query: load rows with category A, limit 2, ordered by ID
3022+ // This should be deduplicated since we already have all category A data
3023+ // So it will load the data from the local collection
3024+ const whereClause2 = new Func ( `eq` , [
3025+ new PropRef ( [ `category` ] ) ,
3026+ new Value ( `A` ) ,
3027+ ] )
3028+ await collection . _sync . loadSubset ( {
3029+ where : whereClause2 ,
3030+ limit : 2 ,
3031+ orderBy : [
3032+ {
3033+ expression : new PropRef ( [ `id` ] ) ,
3034+ compareOptions : {
3035+ direction : `asc` ,
3036+ nulls : `last` ,
3037+ stringSort : `lexical` ,
3038+ } ,
3039+ } ,
3040+ ] ,
3041+ } )
3042+
3043+ // Second query should still only have been called once
3044+ // since query2 is deduplicated so it is executed against the local collection
3045+ // and not via queryFn
3046+ expect ( queryFn ) . toHaveBeenCalledTimes ( 1 )
3047+
3048+ // Collection should still have all 3 items (deduplication doesn't remove data)
3049+ expect ( collection . size ) . toBe ( 3 )
3050+ expect ( collection . has ( `1` ) ) . toBe ( true )
3051+ expect ( collection . has ( `2` ) ) . toBe ( true )
3052+ expect ( collection . has ( `3` ) ) . toBe ( true )
3053+
3054+ // GC the first query (all category A without limit)
3055+ queryClient . removeQueries ( {
3056+ queryKey : ( config . queryKey as any ) ( { where : whereClause1 } ) ,
3057+ exact : true ,
3058+ } )
3059+
3060+ // Wait for GC to process
3061+ await vi . waitFor ( ( ) => {
3062+ expect ( collection . size ) . toBe ( 2 ) // Should only have items 1 and 2 because they are still referenced by query 2
3063+ } )
3064+
3065+ // Verify that only row 3 is removed (it was only referenced by query 1)
3066+ expect ( collection . has ( `1` ) ) . toBe ( true ) // Still present (referenced by query 2)
3067+ expect ( collection . has ( `2` ) ) . toBe ( true ) // Still present (referenced by query 2)
3068+ expect ( collection . has ( `3` ) ) . toBe ( false ) // Removed (only referenced by query 1)
3069+
3070+ // GC the second query (category A with limit 2)
3071+ queryClient . removeQueries ( {
3072+ queryKey : ( config . queryKey as any ) ( {
3073+ where : whereClause2 ,
3074+ limit : 2 ,
3075+ orderBy : [
3076+ {
3077+ expression : new PropRef ( [ `id` ] ) ,
3078+ compareOptions : {
3079+ direction : `asc` ,
3080+ nulls : `last` ,
3081+ stringSort : `lexical` ,
3082+ } ,
3083+ } ,
3084+ ] ,
3085+ } ) ,
3086+ exact : true ,
3087+ } )
3088+
3089+ // Wait for final GC to process
3090+ await vi . waitFor ( ( ) => {
3091+ expect ( collection . size ) . toBe ( 0 )
3092+ } )
3093+ } )
29463094 } )
29473095} )
0 commit comments