11import { LocalFetchOptions , CacheEntry , CacheMetadata } from './types' ;
2- import { getFromStorage , saveToStorage } from './storage' ;
2+ import { getFromStorage , saveToStorage , removeFromStorage } from './storage' ;
33import { encrypt , decrypt } from './crypto' ;
4+ import { cacheEmitter } from './events' ;
45
56const isServer = typeof window === 'undefined' ;
7+ const memoryCache = new Map < string , CacheEntry < any > > ( ) ;
68
79/**
810 * The core fetching engine for react-local-fetch.
@@ -27,8 +29,15 @@ export async function localFetch<T>(
2729 return await response . json ( ) ;
2830 }
2931
30- // 1. Try to get from cache
31- const cached = await getFromStorage < T > ( key ) ;
32+ // 1. Try to get from cache (L1 then L2)
33+ let cached = memoryCache . get ( key ) as CacheEntry < T > | undefined ;
34+
35+ if ( ! cached ) {
36+ cached = await getFromStorage < T > ( key ) ;
37+ if ( cached ) {
38+ memoryCache . set ( key , cached ) ;
39+ }
40+ }
3241
3342 if ( cached ) {
3443 const { metadata, data : storedData } = cached ;
@@ -46,7 +55,7 @@ export async function localFetch<T>(
4655 if ( metadata . isEncrypted ) {
4756 if ( ! secret ) throw new Error ( 'Secret is required to decrypt data.' ) ;
4857 finalData = await decrypt (
49- storedData ,
58+ storedData as ArrayBuffer ,
5059 secret ,
5160 metadata . salt ! ,
5261 metadata . iv !
@@ -67,11 +76,11 @@ export async function localFetch<T>(
6776 // Stale data but fallback allowed
6877 revalidateBackground ( url , options ) ;
6978
70- try {
79+ try {
7180 let finalData : string ;
7281 if ( metadata . isEncrypted ) {
7382 if ( ! secret ) throw new Error ( 'Secret is required to decrypt data.' ) ;
74- finalData = await decrypt ( storedData , secret , metadata . salt ! , metadata . iv ! ) ;
83+ finalData = await decrypt ( storedData as ArrayBuffer , secret , metadata . salt ! , metadata . iv ! ) ;
7584 } else {
7685 finalData = typeof storedData === 'string' ? storedData : JSON . stringify ( storedData ) ;
7786 }
@@ -86,45 +95,71 @@ export async function localFetch<T>(
8695 return await fetchAndStore ( url , options ) ;
8796}
8897
98+ const activeRequests = new Map < string , Promise < any > > ( ) ;
99+
89100/**
90101 * Fetches data from network, encrypts it (if needed), and stores it.
91102 */
92103async function fetchAndStore < T > ( url : string , options : LocalFetchOptions ) : Promise < T > {
93- const { key, version = 0 , encrypt : shouldEncrypt = false , secret, headers = { } } = options ;
104+ const { key, version = 0 , encrypt : shouldEncrypt = false , secret, headers = { } , updateStrategy = 'reactive' } = options ;
94105
95- const response = await fetch ( url , { headers } ) ;
96- if ( ! response . ok ) {
97- throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
98- }
99- const freshData = await response . json ( ) ;
100- const jsonString = JSON . stringify ( freshData ) ;
101-
102- const metadata : CacheMetadata = {
103- timestamp : Date . now ( ) ,
104- version,
105- isEncrypted : shouldEncrypt ,
106- } ;
107-
108- let dataToStore : any ;
109-
110- if ( shouldEncrypt ) {
111- if ( ! secret ) throw new Error ( 'Secret is required to encrypt data.' ) ;
112- const { buffer, salt, iv } = await encrypt ( jsonString , secret ) ;
113- dataToStore = buffer ;
114- metadata . salt = salt ;
115- metadata . iv = iv ;
116- } else {
117- dataToStore = freshData ;
106+ if ( activeRequests . has ( key ) ) {
107+ return activeRequests . get ( key ) as Promise < T > ;
118108 }
119109
120- const entry : CacheEntry < T > = {
121- data : dataToStore ,
122- metadata,
123- } ;
110+ const promise = ( async ( ) => {
111+ const response = await fetch ( url , { headers } ) ;
112+ if ( ! response . ok ) {
113+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
114+ }
115+ const freshData = await response . json ( ) ;
116+ const jsonString = JSON . stringify ( freshData ) ;
117+
118+ const metadata : CacheMetadata = {
119+ timestamp : Date . now ( ) ,
120+ version,
121+ isEncrypted : shouldEncrypt ,
122+ } ;
123+
124+ let dataToStore : any ;
125+
126+ if ( shouldEncrypt ) {
127+ if ( ! secret ) throw new Error ( 'Secret is required to encrypt data.' ) ;
128+ const { buffer, salt, iv } = await encrypt ( jsonString , secret ) ;
129+ dataToStore = buffer ;
130+ metadata . salt = salt ;
131+ metadata . iv = iv ;
132+ } else {
133+ dataToStore = freshData ;
134+ }
135+
136+ const entry : CacheEntry < T > = {
137+ data : dataToStore ,
138+ metadata,
139+ } ;
140+
141+ memoryCache . set ( key , entry ) ;
142+
143+ try {
144+ await saveToStorage ( key , entry ) ;
145+ } catch ( err ) {
146+ console . warn ( 'Failed to save to local storage' , err ) ;
147+ }
148+
149+ if ( updateStrategy !== 'silent' ) {
150+ cacheEmitter . emit ( key ) ;
151+ }
152+
153+ return freshData ;
154+ } ) ( ) ;
124155
125- await saveToStorage ( key , entry ) ;
156+ activeRequests . set ( key , promise ) ;
126157
127- return freshData ;
158+ try {
159+ return await promise ;
160+ } finally {
161+ activeRequests . delete ( key ) ;
162+ }
128163}
129164
130165/**
@@ -137,3 +172,47 @@ async function revalidateBackground(url: string, options: LocalFetchOptions): Pr
137172 console . warn ( `Background sync failed for ${ url } ` , err ) ;
138173 }
139174}
175+
176+ /**
177+ * Mutates the cache for a given key, triggering revalidation in active hooks.
178+ */
179+ export async function mutate < T > (
180+ key : string ,
181+ data ?: T ,
182+ options ?: Partial < LocalFetchOptions >
183+ ) : Promise < void > {
184+ if ( data !== undefined ) {
185+ const jsonString = JSON . stringify ( data ) ;
186+ let dataToStore : any = data ;
187+ const metadata : CacheMetadata = {
188+ timestamp : Date . now ( ) ,
189+ version : options ?. version || 0 ,
190+ isEncrypted : ! ! options ?. encrypt ,
191+ } ;
192+
193+ if ( options ?. encrypt && options ?. secret ) {
194+ const { buffer, salt, iv } = await encrypt ( jsonString , options . secret ) ;
195+ dataToStore = buffer ;
196+ metadata . salt = salt ;
197+ metadata . iv = iv ;
198+ }
199+
200+ const entry : CacheEntry < T > = { data : dataToStore , metadata } ;
201+
202+ memoryCache . set ( key , entry ) ;
203+ try {
204+ await saveToStorage ( key , entry ) ;
205+ } catch ( err ) {
206+ console . warn ( 'mutate failed to save to storage' , err ) ;
207+ }
208+ } else {
209+ memoryCache . delete ( key ) ;
210+ try {
211+ await removeFromStorage ( key ) ;
212+ } catch ( err ) {
213+ console . warn ( 'mutate failed to remove from storage' , err ) ;
214+ }
215+ }
216+
217+ cacheEmitter . emit ( key ) ;
218+ }
0 commit comments