@@ -148,7 +148,7 @@ public final class Flagsmith: @unchecked Sendable {
148148 self . updateFlagStreamAndLastUpdatedAt ( thisIdentity. flags)
149149 completion ( . success( thisIdentity. flags) )
150150 case let . failure( error) :
151- self . handleFlagsError ( error, completion: completion)
151+ self . handleFlagsErrorForIdentity ( error, identity : identity , completion: completion)
152152 }
153153 }
154154 }
@@ -171,13 +171,181 @@ public final class Flagsmith: @unchecked Sendable {
171171 }
172172
173173 private func handleFlagsError( _ error: any Error , completion: @Sendable @escaping ( Result < [ Flag ] , any Error > ) -> Void ) {
174- if defaultFlags. isEmpty {
175- completion ( . failure( error) )
174+ // Priority: 1. Try cached flags, 2. Fall back to default flags, 3. Return error
175+
176+ // First, try to get cached flags if caching is enabled
177+ if cacheConfig. useCache {
178+ if let cachedFlags = getCachedFlags ( ) {
179+ completion ( . success( cachedFlags) )
180+ return
181+ }
182+ }
183+
184+ // If no cached flags available, try default flags
185+ if !defaultFlags. isEmpty {
186+ completion ( . success( defaultFlags) )
176187 } else {
188+ completion ( . failure( error) )
189+ }
190+ }
191+
192+ private func handleFlagsErrorForIdentity( _ error: any Error , identity: String , completion: @Sendable @escaping ( Result < [ Flag ] , any Error > ) -> Void ) {
193+ // Priority: 1. Try cached flags for identity, 2. Try general cached flags, 3. Fall back to default flags, 4. Return error
194+
195+ // First, try to get cached flags for the specific identity if caching is enabled
196+ if cacheConfig. useCache {
197+ if let cachedFlags = getCachedFlags ( forIdentity: identity) {
198+ completion ( . success( cachedFlags) )
199+ return
200+ }
201+
202+ // If no identity-specific cache, try general flags cache
203+ if let cachedFlags = getCachedFlags ( ) {
204+ completion ( . success( cachedFlags) )
205+ return
206+ }
207+ }
208+
209+ // If no cached flags available, try default flags
210+ if !defaultFlags. isEmpty {
177211 completion ( . success( defaultFlags) )
212+ } else {
213+ completion ( . failure( error) )
214+ }
215+ }
216+
217+ private func getCachedFlags( ) -> [ Flag ] ? {
218+ let cache = cacheConfig. cache
219+
220+ // Create request for general flags
221+ let request = URLRequest ( url: baseURL. appendingPathComponent ( " flags/ " ) )
222+
223+ // Check if we have a cached response
224+ if let cachedResponse = cache. cachedResponse ( for: request) {
225+ // Check if cache is still valid based on TTL
226+ if isCacheValid ( cachedResponse: cachedResponse) {
227+ do {
228+ let flags = try JSONDecoder ( ) . decode ( [ Flag ] . self, from: cachedResponse. data)
229+ return flags
230+ } catch {
231+ print ( " Flagsmith - Failed to decode cached flags: \( error. localizedDescription) " )
232+ return nil
233+ }
234+ }
178235 }
236+
237+ return nil
179238 }
239+
240+ private func getCachedFlags( forIdentity identity: String ) -> [ Flag ] ? {
241+ let cache = cacheConfig. cache
242+
243+ // Create request for identity-specific flags
244+ let identityURL = baseURL. appendingPathComponent ( " identities/ " )
245+ guard var components = URLComponents ( url: identityURL, resolvingAgainstBaseURL: false ) else {
246+ return nil
247+ }
248+ components. queryItems = [ URLQueryItem ( name: " identifier " , value: identity) ]
249+
250+ guard let url = components. url else { return nil }
251+ let request = URLRequest ( url: url)
252+
253+ // Check if we have a cached response
254+ if let cachedResponse = cache. cachedResponse ( for: request) {
255+ // Check if cache is still valid based on TTL
256+ if isCacheValid ( cachedResponse: cachedResponse) {
257+ do {
258+ let identity = try JSONDecoder ( ) . decode ( Identity . self, from: cachedResponse. data)
259+ return identity. flags
260+ } catch {
261+ print ( " Flagsmith - Failed to decode cached identity flags: \( error. localizedDescription) " )
262+ return nil
263+ }
264+ }
265+ }
266+
267+ return nil
268+ }
269+
270+ private func isCacheValid( cachedResponse: CachedURLResponse ) -> Bool {
271+ guard let httpResponse = cachedResponse. response as? HTTPURLResponse else { return false }
272+
273+ // Check if we have a cache control header
274+ if let cacheControl = httpResponse. allHeaderFields [ " Cache-Control " ] as? String {
275+ // First check for no-cache and no-store directives (case-insensitive, token-aware)
276+ if hasNoCacheDirective ( in: cacheControl) {
277+ return false
278+ }
279+
280+ if let maxAge = extractMaxAge ( from: cacheControl) {
281+ // Check if cache is still valid based on max-age
282+ if let dateString = httpResponse. allHeaderFields [ " Date " ] as? String ,
283+ let date = HTTPURLResponse . dateFormatter. date ( from: dateString) {
284+ let age = Date ( ) . timeIntervalSince ( date)
285+ return age < maxAge
286+ }
287+ }
288+ }
289+
290+ // If no cache control, validate against configured TTL
291+ if cacheConfig. cacheTTL > 0 {
292+ if let dateString = httpResponse. allHeaderFields [ " Date " ] as? String ,
293+ let date = HTTPURLResponse . dateFormatter. date ( from: dateString) {
294+ let age = Date ( ) . timeIntervalSince ( date)
295+ return age < cacheConfig. cacheTTL
296+ }
297+ // No Date header, be conservative
298+ return false
299+ }
300+ // TTL of 0 means infinite
301+
302+ return true
303+
304+ }
305+
306+ private func extractMaxAge( from cacheControl: String ) -> TimeInterval ? {
307+ let components = cacheControl. split ( separator: " , " )
308+ for component in components {
309+ let trimmed = component. trimmingCharacters ( in: . whitespaces)
310+ if trimmed. hasPrefix ( " max-age= " ) {
311+ let maxAgeString = String ( trimmed. dropFirst ( 8 ) )
312+ return TimeInterval ( maxAgeString)
313+ }
314+ }
315+ return nil
316+ }
317+
318+ private func hasNoCacheDirective( in cacheControl: String ) -> Bool {
319+ let components = cacheControl. split ( separator: " , " )
320+ for component in components {
321+ let trimmed = component. trimmingCharacters ( in: . whitespaces)
322+ let directiveTokens = trimmed. split ( separator: " = " ) . first? . split ( separator: " ; " ) . first
323+ guard let directiveToken = directiveTokens else { continue }
324+
325+ let directive = directiveToken. trimmingCharacters ( in: . whitespaces) . lowercased ( )
326+ if directive == " no-cache " || directive == " no-store " {
327+ return true
328+ }
329+ }
330+ return false
331+ }
332+ }
333+
334+ // MARK: - HTTPURLResponse Extensions
335+
336+ extension HTTPURLResponse {
337+ static let dateFormatter : DateFormatter = {
338+ let formatter = DateFormatter ( )
339+ formatter. dateFormat = " EEE, dd MMM yyyy HH:mm:ss zzz "
340+ formatter. locale = Locale ( identifier: " en_US_POSIX " )
341+ formatter. timeZone = TimeZone ( abbreviation: " GMT " )
342+ return formatter
343+ } ( )
344+ }
345+
346+ // MARK: - Public API Methods
180347
348+ extension Flagsmith {
181349 /// Check feature exists and is enabled optionally for a specific identity
182350 ///
183351 /// - Parameters:
0 commit comments