Skip to content

Commit ead8916

Browse files
author
陈家名
committed
Normalize activity query cache keys
1 parent 83b334c commit ead8916

File tree

3 files changed

+117
-83
lines changed

3 files changed

+117
-83
lines changed

cli/src/hooks/__tests__/use-activity-query.test.ts

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)