Skip to content

Commit 4db1c3b

Browse files
authored
Merge pull request #99 from levochkaa/improve-caching
Improve caching: custom CachingLayer and encryption by default
2 parents d99d837 + 031b8aa commit 4db1c3b

File tree

6 files changed

+44
-40
lines changed

6 files changed

+44
-40
lines changed

GrowthBookTests/FeaturesViewModelTests.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class FeaturesViewModelTests: XCTestCase, FeaturesFlowDelegate {
88
var isError: Bool = false
99
var hasFeatures: Bool = false
1010

11-
let cachingManager = CachingManager()
11+
let cachingManager: CachingLayer = CachingManager()
1212

1313
override func setUp() {
1414
super.setUp()
@@ -87,7 +87,8 @@ class FeaturesViewModelTests: XCTestCase, FeaturesFlowDelegate {
8787

8888
let viewModel = FeaturesViewModel(delegate: self, dataSource: FeaturesDataSource(dispatcher: MockNetworkClient(successResponse: MockResponse().successResponseEncryptedFeatures, error: nil)), cachingManager: cachingManager)
8989

90-
viewModel.encryptionKey = "3tfeoyW0wlo47bDnbWDkxg=="
90+
let encryptionKey = "3tfeoyW0wlo47bDnbWDkxg=="
91+
viewModel.encryptionKey = encryptionKey
9192
viewModel.fetchFeatures(apiUrl: "")
9293

9394
let cachingManager: CachingLayer = CachingManager()
@@ -97,7 +98,10 @@ class FeaturesViewModelTests: XCTestCase, FeaturesFlowDelegate {
9798
return
9899
}
99100

100-
if let _ = try? JSONDecoder().decode(Features.self, from: featureData) {
101+
let crypto: CryptoProtocol = Crypto()
102+
if let encryptedString = String(data: featureData, encoding: .utf8), crypto.getFeaturesFromEncryptedFeatures(encryptedString: encryptedString, encryptionKey: encryptionKey) != nil {
103+
XCTAssertTrue(true)
104+
} else if let _ = try? JSONDecoder().decode(Features.self, from: featureData) {
101105
XCTAssertTrue(true)
102106
} else {
103107
XCTFail()

GrowthBookTests/GrowthBookSDKBuilderTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class GrowthBookSDKBuilderTests: XCTestCase {
99
let testAttributes: JSON = JSON()
1010
let testKeyString = "Ns04T5n9+59rl2x3SlNHtQ=="
1111

12-
let cachingManager = CachingManager(apiKey: "4r23r324f23")
12+
let cachingManager: CachingLayer = CachingManager(apiKey: "4r23r324f23")
1313

1414
final class RefreshFlag: @unchecked Sendable {
1515
private let lock = NSLock()

Sources/CommonMain/Caching/CachingManager.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import Foundation
22
import CommonCrypto
33

44
/// Interface for Caching Layer
5-
public protocol CachingLayer: AnyObject {
5+
@objc public protocol CachingLayer: AnyObject {
66
func saveContent(fileName: String, content: Data)
77
func getContent(fileName: String) -> Data?
88
func setCacheKey(_ key: String)
9+
func clearCache()
10+
func setSystemCacheDirectory(_ directory: CacheDirectory)
11+
func setCustomCachePath(_ path: String)
912
}
1013

1114
/// This is actual implementation of Caching Layer in iOS
@@ -35,14 +38,6 @@ public protocol CachingLayer: AnyObject {
3538
let key = hash.map { String(format: "%02x", $0) }.joined()
3639
return String(key.prefix(5))
3740
}
38-
39-
@objc func getData(fileName: String) -> Data? {
40-
return getContent(fileName: fileName)
41-
}
42-
43-
@objc func putData(fileName: String, content: Data) {
44-
saveContent(fileName: fileName, content: content)
45-
}
4641

4742
/// Set a custom cache saving directory
4843
@objc public func setCustomCachePath(_ path: String) {

Sources/CommonMain/Features/FeaturesViewModel.swift

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ class FeaturesViewModel {
1515
let dataSource: FeaturesDataSource
1616
var encryptionKey: String?
1717
/// Caching Manager
18-
let manager: CachingManager
18+
let manager: CachingLayer
1919

20-
init(delegate: FeaturesFlowDelegate, dataSource: FeaturesDataSource, cachingManager: CachingManager) {
20+
init(delegate: FeaturesFlowDelegate, dataSource: FeaturesDataSource, cachingManager: CachingLayer) {
2121
self.delegate = delegate
2222
self.dataSource = dataSource
2323
self.manager = cachingManager
@@ -40,37 +40,35 @@ class FeaturesViewModel {
4040
}
4141
}
4242

43-
private func fetchCachedFeatures() {
43+
private func fetchCachedFeatures(logging: Bool = false) {
4444
// Check for cache data
45-
if let json = manager.getData(fileName: Constants.featureCache) {
45+
if let data = manager.getContent(fileName: Constants.featureCache) {
4646
let decoder = JSONDecoder()
47-
if let features = try? decoder.decode(Features.self, from: json) {
47+
if let encryptedString = String(data: data, encoding: .utf8), let encryptionKey, !encryptionKey.isEmpty {
48+
let crypto: CryptoProtocol = Crypto()
49+
if let features = crypto.getFeaturesFromEncryptedFeatures(encryptedString: encryptedString, encryptionKey: encryptionKey) {
50+
delegate?.featuresFetchedSuccessfully(features: features, isRemote: false)
51+
} else {
52+
delegate?.featuresFetchFailed(error: .failedParsedEncryptedData, isRemote: false)
53+
if logging { logger.error("Failed get features from cached encrypted features") }
54+
}
55+
} else if let features = try? decoder.decode(Features.self, from: data) {
4856
// Call Success Delegate with mention of data available but its not remote
4957
delegate?.featuresFetchedSuccessfully(features: features, isRemote: false)
5058
} else {
5159
delegate?.featuresFetchFailed(error: .failedParsedData, isRemote: false)
60+
if logging { logger.error("Failed parse local data") }
5261
}
5362
} else {
5463
delegate?.featuresFetchFailed(error: .failedToLoadData, isRemote: false)
64+
if logging { logger.info("Cache directory is empty. Nothing to fetch.") }
5565
}
5666
}
5767

5868
/// Fetch Features
5969
func fetchFeatures(apiUrl: String?, remoteEval: Bool = false, payload: RemoteEvalParams? = nil) {
6070
// Check for cache data
61-
if let json = manager.getData(fileName: Constants.featureCache) {
62-
let decoder = JSONDecoder()
63-
if let features = try? decoder.decode(Features.self, from: json) {
64-
// Call Success Delegate with mention of data available but its not remote
65-
delegate?.featuresFetchedSuccessfully(features: features, isRemote: false)
66-
} else {
67-
delegate?.featuresFetchFailed(error: .failedParsedData, isRemote: false)
68-
logger.error("Failed parse local data")
69-
}
70-
} else {
71-
delegate?.featuresFetchFailed(error: .failedToLoadData, isRemote: false)
72-
logger.info("Cache directory is empty. Nothing to fetch.")
73-
}
71+
fetchCachedFeatures(logging: true)
7472

7573
if let apiUrl = apiUrl {
7674
if remoteEval {
@@ -111,8 +109,8 @@ class FeaturesViewModel {
111109
if let encryptionKey = encryptionKey, !encryptionKey.isEmpty {
112110
let crypto: CryptoProtocol = Crypto()
113111
if let features = crypto.getFeaturesFromEncryptedFeatures(encryptedString: encryptedString, encryptionKey: encryptionKey) {
114-
if let featureData = try? JSONEncoder().encode(features) {
115-
manager.putData(fileName: Constants.featureCache, content: featureData)
112+
if let featureData = encryptedString.data(using: .utf8) {
113+
manager.saveContent(fileName: Constants.featureCache, content: featureData)
116114
} else {
117115
logger.error("Failed encode features")
118116
}
@@ -129,7 +127,7 @@ class FeaturesViewModel {
129127
}
130128
} else if let features = jsonPetitions.features {
131129
if let featureData = try? JSONEncoder().encode(features) {
132-
manager.putData(fileName: Constants.featureCache, content: featureData)
130+
manager.saveContent(fileName: Constants.featureCache, content: featureData)
133131
}
134132
delegate?.featuresFetchedSuccessfully(features: features, isRemote: true)
135133
} else {
@@ -141,8 +139,8 @@ class FeaturesViewModel {
141139
if let encryptedSavedGroups = jsonPetitions.encryptedSavedGroups, !encryptedSavedGroups.isEmpty, let encryptionKey = encryptionKey, !encryptionKey.isEmpty {
142140
let crypto = Crypto()
143141
if let savedGroups = crypto.getSavedGroupsFromEncryptedFeatures(encryptedString: encryptedSavedGroups, encryptionKey: encryptionKey) {
144-
if let encryptedSavedGroups = try? JSONEncoder().encode(savedGroups) {
145-
manager.putData(fileName: Constants.savedGroupsCache, content: encryptedSavedGroups)
142+
if let encryptedSavedGroups = encryptedSavedGroups.data(using: .utf8) {
143+
manager.saveContent(fileName: Constants.savedGroupsCache, content: encryptedSavedGroups)
146144
} else {
147145
logger.error("Failed encode saved groups")
148146
}
@@ -154,7 +152,7 @@ class FeaturesViewModel {
154152
}
155153
} else if let savedGroups = jsonPetitions.savedGroups {
156154
if let savedGroupsData = try? JSONEncoder().encode(savedGroups) {
157-
manager.putData(fileName: Constants.savedGroupsCache, content: savedGroupsData)
155+
manager.saveContent(fileName: Constants.savedGroupsCache, content: savedGroupsData)
158156
}
159157
delegate?.savedGroupsFetchedSuccessfully(savedGroups: savedGroups, isRemote: true)
160158
}

Sources/CommonMain/GrowthBookSDK.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public struct GrowthBookModel {
4040
private var refreshHandler: CacheRefreshHandler?
4141
private var networkDispatcher: NetworkProtocol = CoreNetworkClient()
4242

43-
private var cachingManager: CachingManager
43+
private var cachingManager: CachingLayer
4444

4545
@objc public init(apiHost: String? = nil, clientKey: String? = nil, encryptionKey: String? = nil, attributes: [String: Any], trackingCallback: @escaping TrackingCallback, refreshHandler: CacheRefreshHandler? = nil, backgroundSync: Bool = false, remoteEval: Bool = false, apiRequestHeaders: [String: String]? = nil, streamingHostRequestHeaders: [String: String]? = nil) {
4646
growthBookBuilderModel = GrowthBookModel(apiHost: apiHost, clientKey: clientKey, encryptionKey: encryptionKey, attributes: JSON(attributes), trackingClosure: trackingCallback, backgroundSync: backgroundSync, remoteEval: remoteEval, apiRequestHeaders: apiRequestHeaders, streamingHostRequestHeaders: streamingHostRequestHeaders)
@@ -84,6 +84,12 @@ public struct GrowthBookModel {
8484
return self
8585
}
8686

87+
/// Set Caching Manager - Caching Client for saving fetched features
88+
@objc public func setCachingManager(cachingManager: CachingLayer) -> GrowthBookBuilder {
89+
self.cachingManager = cachingManager
90+
return self
91+
}
92+
8793
@objc public func setStickyBucketService(stickyBucketService: StickyBucketServiceProtocol? = StickyBucketService()) -> GrowthBookBuilder {
8894
growthBookBuilderModel.stickyBucketService = stickyBucketService
8995
return self
@@ -162,15 +168,15 @@ public struct GrowthBookModel {
162168
private var attributeOverrides: JSON = JSON()
163169
private var savedGroupsValues: JSON?
164170
private var evalContext: EvalContext? = nil
165-
var cachingManager: CachingManager
171+
var cachingManager: CachingLayer
166172

167173
init(context: Context,
168174
refreshHandler: CacheRefreshHandler? = nil,
169175
logLevel: Level = .info,
170176
networkDispatcher: NetworkProtocol = CoreNetworkClient(),
171177
features: Features? = nil,
172178
savedGroups: JSON? = nil,
173-
cachingManager: CachingManager) {
179+
cachingManager: CachingLayer) {
174180
gbContext = context
175181
self.refreshHandler = refreshHandler
176182
self.networkDispatcher = networkDispatcher

Sources/CommonMain/Utils/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public struct BucketRange: Codable {
7272
case failedMissingKey = 2
7373
case failedEncryptedFeatures = 3
7474
case failedEncryptedSavedGroups = 4
75+
case failedParsedEncryptedData = 5
7576
}
7677

7778
/// Meta info about the variations

0 commit comments

Comments
 (0)