@@ -21,6 +21,8 @@ export interface QueryResult<T> {
2121 refetch ?: ( ) => Promise < void > ;
2222}
2323
24+ type QueryArgs = Record < string , unknown > | undefined ;
25+
2426const globalCache = new SessionStorageCache ( 'query-cache' ) ;
2527const inFlightRequests = new Map < string , Promise < unknown > > ( ) ;
2628
@@ -41,8 +43,14 @@ async function dedupedFetch<TResult>(
4143 }
4244}
4345
44- type Listener = ( key : string ) => void ;
46+ type CacheEvent =
47+ | { type : 'invalidate' ; key : string }
48+ | { type : 'update' ; key : string } ;
49+
50+ type Listener = ( event : CacheEvent ) => void ;
4551const listeners = new Set < Listener > ( ) ;
52+ let pendingEvents : CacheEvent [ ] = [ ] ;
53+ let cacheFlushScheduled = false ;
4654
4755function subscribeToCacheInvalidation ( listener : Listener ) {
4856 listeners . add ( listener ) ;
@@ -51,8 +59,60 @@ function subscribeToCacheInvalidation(listener: Listener) {
5159 } ;
5260}
5361
62+ function flushCacheEvents ( ) {
63+ cacheFlushScheduled = false ;
64+ const events = pendingEvents ;
65+ pendingEvents = [ ] ;
66+
67+ for ( const event of events ) {
68+ listeners . forEach ( ( listener ) => listener ( event ) ) ;
69+ }
70+ }
71+
72+ function enqueueCacheEvent ( event : CacheEvent ) {
73+ pendingEvents . push ( event ) ;
74+
75+ if ( cacheFlushScheduled ) {
76+ return ;
77+ }
78+
79+ cacheFlushScheduled = true ;
80+ queueMicrotask ( flushCacheEvents ) ;
81+ }
82+
5483function notifyInvalidation ( key : string ) {
55- listeners . forEach ( ( listener ) => listener ( key ) ) ;
84+ enqueueCacheEvent ( { type : 'invalidate' , key } ) ;
85+ }
86+
87+ function notifyUpdate ( key : string ) {
88+ enqueueCacheEvent ( { type : 'update' , key } ) ;
89+ }
90+
91+ function buildQueryKey ( queryConfig : QueryConfig , queryArgs : QueryArgs ) : string {
92+ return `${ queryConfig . url } :${ JSON . stringify ( queryArgs || { } ) } ` ;
93+ }
94+
95+ function matchesCacheKey ( eventKey : string , currentKey : string ) : boolean {
96+ return eventKey === currentKey || ( eventKey . endsWith ( '*' ) && currentKey . startsWith ( eventKey . slice ( 0 , - 1 ) ) ) ;
97+ }
98+
99+ function parseCacheKey ( key : string ) : { url : string ; args : QueryArgs } | null {
100+ const separatorIndex = key . indexOf ( ':' ) ;
101+ if ( separatorIndex === - 1 ) {
102+ return null ;
103+ }
104+
105+ const url = key . slice ( 0 , separatorIndex ) ;
106+ const rawArgs = key . slice ( separatorIndex + 1 ) ;
107+
108+ try {
109+ return {
110+ url,
111+ args : rawArgs ? ( JSON . parse ( rawArgs ) as QueryArgs ) : undefined ,
112+ } ;
113+ } catch {
114+ return null ;
115+ }
56116}
57117
58118/**
@@ -77,7 +137,7 @@ export function useCachedQuery<TResult>(
77137 let isStale = $state ( false ) ;
78138
79139 const getArgs = ( ) => ( typeof queryArgs === 'function' ? queryArgs ( ) : queryArgs ) ;
80- const getCacheKey = ( ) => cacheKey || ` ${ queryConfig . url } : ${ JSON . stringify ( getArgs ( ) ) } ` ;
140+ const getCacheKey = ( ) => cacheKey || buildQueryKey ( queryConfig , getArgs ( ) ) ;
81141 const isEnabled = ( ) => ( typeof enabled === 'function' ? enabled ( ) : enabled ) ;
82142
83143 async function fetchData ( ) {
@@ -161,9 +221,22 @@ export function useCachedQuery<TResult>(
161221
162222 // Subscribe to cache invalidations
163223 $effect ( ( ) => {
164- const unsubscribe = subscribeToCacheInvalidation ( ( key ) => {
224+ const unsubscribe = subscribeToCacheInvalidation ( ( event ) => {
165225 const currentKey = getCacheKey ( ) ;
166- if ( key === currentKey || ( key . endsWith ( '*' ) && currentKey . startsWith ( key . slice ( 0 , - 1 ) ) ) ) {
226+ if ( ! matchesCacheKey ( event . key , currentKey ) ) {
227+ return ;
228+ }
229+
230+ if ( event . type === 'update' ) {
231+ const cached = globalCache . get ( currentKey ) ;
232+ data = cached as TResult | undefined ;
233+ error = undefined ;
234+ isLoading = false ;
235+ isStale = false ;
236+ return ;
237+ }
238+
239+ if ( event . type === 'invalidate' ) {
167240 fetchData ( ) ;
168241 }
169242 } ) ;
@@ -304,11 +377,62 @@ export const api = {
304377} as const ;
305378
306379export function invalidateQuery ( query : QueryConfig , queryArgs ?: unknown ) : void {
307- const key = ` ${ query . url } : ${ JSON . stringify ( queryArgs || { } ) } ` ;
380+ const key = buildQueryKey ( query , ( queryArgs as QueryArgs ) ?? undefined ) ;
308381 globalCache . delete ( key ) ;
309382 notifyInvalidation ( key ) ;
310383}
311384
385+ export function setCachedQueryData < TResult > (
386+ query : QueryConfig ,
387+ queryArgs : QueryArgs ,
388+ updater : TResult | ( ( current : TResult | undefined ) => TResult | undefined ) ,
389+ options : { ttl ?: number } = { }
390+ ) : void {
391+ const key = buildQueryKey ( query , queryArgs ) ;
392+ const current = globalCache . get ( key ) as TResult | undefined ;
393+ const next =
394+ typeof updater === 'function'
395+ ? ( updater as ( current : TResult | undefined ) => TResult | undefined ) ( current )
396+ : updater ;
397+
398+ if ( next === undefined ) {
399+ globalCache . delete ( key ) ;
400+ } else {
401+ globalCache . set ( key , next , options . ttl ) ;
402+ }
403+
404+ notifyUpdate ( key ) ;
405+ }
406+
407+ export function setCachedQueryDataMatching < TResult > (
408+ matcher : ( entry : { key : string ; url : string ; args : QueryArgs } ) => boolean ,
409+ updater : ( current : TResult | undefined , entry : { key : string ; url : string ; args : QueryArgs } ) => TResult | undefined ,
410+ options : { ttl ?: number } = { }
411+ ) : void {
412+ for ( const key of globalCache . keys ( ) ) {
413+ const parsed = parseCacheKey ( key ) ;
414+ if ( ! parsed ) {
415+ continue ;
416+ }
417+
418+ const entry = { key, url : parsed . url , args : parsed . args } ;
419+ if ( ! matcher ( entry ) ) {
420+ continue ;
421+ }
422+
423+ const current = globalCache . get ( key ) as TResult | undefined ;
424+ const next = updater ( current , entry ) ;
425+
426+ if ( next === undefined ) {
427+ globalCache . delete ( key ) ;
428+ } else {
429+ globalCache . set ( key , next , options . ttl ) ;
430+ }
431+
432+ notifyUpdate ( key ) ;
433+ }
434+ }
435+
312436export function invalidateQueryPattern ( urlPrefix : string ) : void {
313437 const keys = Array . from ( globalCache . keys ( ) ) ;
314438 for ( const key of keys ) {
0 commit comments