@@ -131,6 +131,14 @@ describe('use-activity-query utilities', () => {
131131 )
132132 } )
133133
134+ test ( 'object key property order does not create duplicate cache entries' , ( ) => {
135+ setActivityQueryData ( [ 'query' , { page : 1 , sort : 'asc' } ] , 'page1' )
136+
137+ expect (
138+ getActivityQueryData < string > ( [ 'query' , { sort : 'asc' , page : 1 } ] ) ,
139+ ) . toBe ( 'page1' )
140+ } )
141+
134142 test ( 'nested objects in keys work correctly' , ( ) => {
135143 setActivityQueryData (
136144 [ 'query' , { filter : { status : 'active' , type : 'user' } } ] ,
@@ -143,6 +151,20 @@ describe('use-activity-query utilities', () => {
143151 ] ) ,
144152 ) . toBe ( 'filtered' )
145153 } )
154+
155+ test ( 'nested object property order does not create duplicate cache entries' , ( ) => {
156+ setActivityQueryData (
157+ [ 'query' , { page : 1 , filter : { status : 'active' , type : 'user' } } ] ,
158+ 'filtered' ,
159+ )
160+
161+ expect (
162+ getActivityQueryData < string > ( [
163+ 'query' ,
164+ { filter : { type : 'user' , status : 'active' } , page : 1 } ,
165+ ] ) ,
166+ ) . toBe ( 'filtered' )
167+ } )
146168 } )
147169} )
148170
@@ -419,88 +441,83 @@ describe('polling and staleness simulation', () => {
419441
420442 test ( 'data becomes stale after staleTime passes' , ( ) => {
421443 const testKey = [ 'stale-time-test' ]
422- const serializedKey = JSON . stringify ( testKey )
423444 const staleTime = 30000 // 30 seconds
424445
425446 // Set data at t=0
426447 setActivityQueryData ( testKey , 'fresh-data' )
427448
428449 // Data was set at mockNow (1000000), so dataUpdatedAt = 1000000
429450 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'fresh-data' )
430- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false ) // Fresh
451+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false ) // Fresh
431452
432453 // Advance time by 25 seconds - still fresh
433454 mockNow += 25000
434- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
455+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
435456
436457 // Advance time past staleTime
437458 mockNow += 10000 // Now 35 seconds have passed
438459 // Data should now be considered stale (35s > 30s staleTime)
439- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
460+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true )
440461
441462 // The data is still accessible even when stale
442463 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'fresh-data' )
443464 } )
444465
445466 test ( 'invalidated data is immediately stale' , ( ) => {
446467 const testKey = [ 'invalidate-stale-test' ]
447- const serializedKey = JSON . stringify ( testKey )
448468 const staleTime = 30000
449469
450470 // Set fresh data
451471 setActivityQueryData ( testKey , 'data' )
452- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
472+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
453473
454474 // Invalidate immediately makes it stale (dataUpdatedAt = 0)
455475 invalidateActivityQuery ( testKey )
456- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
476+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true )
457477
458478 // Data still exists but would be refetched on next access
459479 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'data' )
460480 } )
461481
462482 test ( 'updating data resets the staleness timer' , ( ) => {
463483 const testKey = [ 'reset-timer-test' ]
464- const serializedKey = JSON . stringify ( testKey )
465484 const staleTime = 30000
466485
467486 // Set initial data
468487 setActivityQueryData ( testKey , 'initial' )
469- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
488+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
470489
471490 // Advance time past staleTime
472491 mockNow += 35000
473- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
492+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true )
474493
475494 // Update data - should reset the timer
476495 setActivityQueryData ( testKey , 'updated' )
477- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false ) // Fresh again
496+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false ) // Fresh again
478497
479498 // Data is fresh again
480499 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'updated' )
481500
482501 // Advance a little bit - should still be fresh
483502 mockNow += 10000
484- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
503+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
485504 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'updated' )
486505 } )
487506
488507 test ( 'staleTime of 0 means always stale' , ( ) => {
489508 const testKey = [ 'zero-stale-test' ]
490- const serializedKey = JSON . stringify ( testKey )
491509
492510 // Set data
493511 setActivityQueryData ( testKey , 'data' )
494512
495513 // With staleTime=0, data is always considered stale
496514 // (this means refetch should happen on every interval tick)
497- expect ( isEntryStale ( serializedKey , 0 ) ) . toBe ( true )
515+ expect ( isEntryStale ( testKey , 0 ) ) . toBe ( true )
498516 expect ( getActivityQueryData < string > ( testKey ) ) . toBe ( 'data' )
499517 } )
500518
501519 test ( 'non-existent key is always stale' , ( ) => {
502- const serializedKey = JSON . stringify ( [ 'non-existent' ] )
503- expect ( isEntryStale ( serializedKey , 30000 ) ) . toBe ( true )
520+ expect ( isEntryStale ( [ 'non-existent' ] , 30000 ) ) . toBe ( true )
504521 } )
505522} )
506523
@@ -624,7 +641,7 @@ describe('Claude subscription update scenarios', () => {
624641 mockNow += 35000 // 35 seconds
625642
626643 // Data is now stale, polling tick should trigger refetch
627- // In real code: if (isEntryStale(serializedKey , staleTime)) void doFetch()
644+ // In real code: if (isEntryStale(testKey , staleTime)) void doFetch()
628645
629646 // Simulate what refetch would do
630647 const newQuota = { fiveHourRemaining : 75 , sevenDayRemaining : 95 }
@@ -809,7 +826,6 @@ describe('error-only entries and persistent error handling', () => {
809826 // 5. This prevents immediate refetch loop
810827
811828 const testKey = [ 'error-only-fresh-test' ]
812- const serializedKey = JSON . stringify ( testKey )
813829 const staleTime = 30000 // 30 seconds
814830 const error = new Error ( 'API error' )
815831
@@ -819,28 +835,27 @@ describe('error-only entries and persistent error handling', () => {
819835 // Entry has errorUpdatedAt = 1000000, current time = 1000000
820836 // Time since error: 0ms, staleTime: 30000ms
821837 // Should NOT be stale because error is recent
822- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
838+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
823839 } )
824840
825841 test ( 'error-only entry becomes stale after staleTime passes' , ( ) => {
826842 const testKey = [ 'error-stale-after-time-test' ]
827- const serializedKey = JSON . stringify ( testKey )
828843 const staleTime = 30000 // 30 seconds
829844 const error = new Error ( 'API error' )
830845
831846 // Create error-only entry at current time
832847 setErrorOnlyCacheEntry ( testKey , error , mockNow )
833848
834849 // Initially not stale
835- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
850+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
836851
837852 // Advance time by 25 seconds - still fresh
838853 mockNow += 25000
839- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
854+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false )
840855
841856 // Advance time past staleTime (now 35 seconds since error)
842857 mockNow += 10000
843- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
858+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true )
844859 } )
845860
846861 test ( 'simulates subscription query polling with persistent errors' , ( ) => {
@@ -851,7 +866,6 @@ describe('error-only entries and persistent error handling', () => {
851866 // - With fix: isEntryStale uses errorUpdatedAt, preventing rapid refetches
852867
853868 const subscriptionKey = [ 'subscription' , 'current' ]
854- const serializedKey = JSON . stringify ( subscriptionKey )
855869 const staleTime = 30000 // 30 seconds (matches useSubscriptionQuery)
856870 const refetchInterval = 60000 // 60 seconds
857871 const error = new Error ( 'Failed to fetch subscription: 500' )
@@ -861,75 +875,72 @@ describe('error-only entries and persistent error handling', () => {
861875
862876 // Immediately after error, entry should NOT be stale
863877 // This is the critical fix - prevents immediate refetch loop
864- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
878+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( false )
865879
866880 // Simulate polling interval at t=1s (as reported in bug)
867881 mockNow += 1000
868882 // Entry should still NOT be stale (only 1s since error, staleTime is 30s)
869- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
883+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( false )
870884
871885 // Simulate many 1-second intervals - none should trigger refetch until staleTime
872886 for ( let i = 0 ; i < 28 ; i ++ ) {
873887 mockNow += 1000
874- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
888+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( false )
875889 }
876890
877891 // Now at t=29s - should still be fresh (29s is not > 30s)
878- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
892+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( false )
879893
880894 // At t=30s - should still be fresh (30s is not > 30s, need strictly greater)
881895 mockNow += 1000
882- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false )
896+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( false )
883897
884898 // At t=31s - now stale, refetch should be allowed (31s > 30s)
885899 mockNow += 1000
886- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
900+ expect ( isEntryStale ( subscriptionKey , staleTime ) ) . toBe ( true )
887901 } )
888902
889903 test ( 'staleTime of 0 means always stale even for error-only entries' , ( ) => {
890904 const testKey = [ 'zero-stale-error-test' ]
891- const serializedKey = JSON . stringify ( testKey )
892905 const error = new Error ( 'Some error' )
893906
894907 setErrorOnlyCacheEntry ( testKey , error , mockNow )
895-
908+
896909 // With staleTime=0, entry is always considered stale
897- expect ( isEntryStale ( serializedKey , 0 ) ) . toBe ( true )
910+ expect ( isEntryStale ( testKey , 0 ) ) . toBe ( true )
898911 } )
899912
900913 test ( 'error-only entry with null errorUpdatedAt is stale' , ( ) => {
901914 // Edge case: if somehow errorUpdatedAt is null, entry should be stale
902915 // This shouldn't happen in practice but tests defensive coding
903916 const testKey = [ 'null-error-time-test' ]
904- const serializedKey = JSON . stringify ( testKey )
905917 const staleTime = 30000
906918
907919 // Create entry without errorUpdatedAt (using undefined which gets stored as null)
908920 // Note: setErrorOnlyCacheEntry always sets errorUpdatedAt, so we test via regular data
909921 // and then invalidate it
910-
922+
911923 // Non-existent key is stale
912- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true )
924+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true )
913925 } )
914926
915927 test ( 'successful data takes precedence over errorUpdatedAt for staleness' , ( ) => {
916928 const testKey = [ 'data-precedence-test' ]
917- const serializedKey = JSON . stringify ( testKey )
918929 const staleTime = 30000
919930
920931 // First, set an error-only entry
921932 setErrorOnlyCacheEntry ( testKey , new Error ( 'Initial error' ) , mockNow )
922- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false ) // Fresh error
933+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false ) // Fresh error
923934
924935 // Now set successful data (this is what happens on successful retry)
925936 setActivityQueryData ( testKey , { subscription : 'active' } )
926-
937+
927938 // Staleness should now be based on dataUpdatedAt, not errorUpdatedAt
928- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( false ) // Fresh data
939+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( false ) // Fresh data
929940
930941 // Advance time past staleTime
931942 mockNow += 35000
932- expect ( isEntryStale ( serializedKey , staleTime ) ) . toBe ( true ) // Stale based on dataUpdatedAt
943+ expect ( isEntryStale ( testKey , staleTime ) ) . toBe ( true ) // Stale based on dataUpdatedAt
933944 } )
934945} )
935946
@@ -1105,10 +1116,9 @@ describe('retry infinite loop bug fix (subscription 401 scenario)', () => {
11051116
11061117 // Error entry should exist (data is undefined but entry exists)
11071118 // The entry has error set, which we can verify via isEntryStale behavior
1108- const serializedKey = JSON . stringify ( queryKey )
11091119 // Entry exists (not stale due to "no entry" - stale due to other reasons)
11101120 // Since we just set errorUpdatedAt = Date.now(), it should not be stale
11111121 // for a reasonable staleTime
1112- expect ( isEntryStale ( serializedKey , 30000 ) ) . toBe ( false )
1122+ expect ( isEntryStale ( queryKey , 30000 ) ) . toBe ( false )
11131123 } )
11141124} )
0 commit comments