@@ -12,20 +12,27 @@ protocol FeaturesFlowDelegate: AnyObject {
1212/// View Model for Features
1313class FeaturesViewModel {
1414 weak var delegate : FeaturesFlowDelegate ?
15- let dataSource : FeaturesDataSource
1615 var encryptionKey : String ?
17- /// Caching Manager
18- let manager : CachingLayer
19- /// SSE Handler for background sync
20- internal var sseHandler : SSEHandler ?
16+
17+ private let dataSource : FeaturesDataSource
18+ private let manager : CachingLayer
19+ private var sseHandler : SSEHandler ?
20+ private let fileSaveQueue = DispatchQueue ( label: " com.sdk.fileSaveQueue " , qos: . utility)
21+
22+ var forceSynchronousSave : Bool
2123
22- init ( delegate: FeaturesFlowDelegate , dataSource: FeaturesDataSource , cachingManager: CachingLayer ) {
24+ init ( delegate: FeaturesFlowDelegate , dataSource: FeaturesDataSource , cachingManager: CachingLayer , forceSynchronousSave : Bool = false ) {
2325 self . delegate = delegate
2426 self . dataSource = dataSource
2527 self . manager = cachingManager
28+ self . forceSynchronousSave = forceSynchronousSave
2629 self . fetchCachedFeatures ( )
2730 }
2831
32+ deinit {
33+ sseHandler? . disconnect ( )
34+ }
35+
2936 func connectBackgroundSync( sseUrl: String ) {
3037 guard let url = URL ( string: sseUrl) else { return }
3138
@@ -35,144 +42,171 @@ class FeaturesViewModel {
3542 let streamingUpdate = SSEHandler ( url: url)
3643 sseHandler = streamingUpdate
3744
38- streamingUpdate. addEventListener ( event: " features " ) { [ weak self] id , event , data in
39- guard let jsonData = data? . data ( using: . utf8) else { return }
40- self ? . prepareFeaturesData ( data: jsonData)
45+ streamingUpdate. addEventListener ( event: " features " ) { [ weak self] _ , _ , data in
46+ guard let self , let jsonData = data? . data ( using: . utf8) else { return }
47+ self . prepareFeaturesData ( data: jsonData)
4148 }
4249 streamingUpdate. connect ( )
4350
4451 streamingUpdate. onDissconnect { [ weak streamingUpdate] _, shouldReconnect, _ in
45- if let shouldReconnect = shouldReconnect , shouldReconnect {
52+ if shouldReconnect == true {
4653 streamingUpdate? . connect ( )
4754 }
4855 }
4956 }
50-
51- deinit {
52- sseHandler? . disconnect ( )
53- }
54-
55- private func fetchCachedFeatures( logging: Bool = false ) {
56- // Check for cache data
57- if let data = manager. getContent ( fileName: Constants . featureCache) {
58- let decoder = JSONDecoder ( )
59- if let encryptedString = String ( data: data, encoding: . utf8) , let encryptionKey, !encryptionKey. isEmpty {
60- let crypto : CryptoProtocol = Crypto ( )
61- if let features = crypto. getFeaturesFromEncryptedFeatures ( encryptedString: encryptedString, encryptionKey: encryptionKey) {
62- delegate? . featuresFetchedSuccessfully ( features: features, isRemote: false )
63- } else {
64- delegate? . featuresFetchFailed ( error: . failedParsedEncryptedData, isRemote: false )
65- if logging { logger. error ( " Failed get features from cached encrypted features " ) }
66- }
67- } else if let features = try ? decoder. decode ( Features . self, from: data) {
68- // Call Success Delegate with mention of data available but its not remote
69- delegate? . featuresFetchedSuccessfully ( features: features, isRemote: false )
70- } else {
71- delegate? . featuresFetchFailed ( error: . failedParsedData, isRemote: false )
72- if logging { logger. error ( " Failed parse local data " ) }
57+
58+ func fetchFeatures( apiUrl: String ? , remoteEval: Bool = false , payload: RemoteEvalParams ? = nil ) {
59+ fetchCachedFeatures ( logging: true )
60+
61+ guard let apiUrl else {
62+ logger. error ( " Missing API URL " )
63+ notify { $0. featuresFetchFailed ( error: . failedMissingKey, isRemote: true ) }
64+ return
65+ }
66+
67+ let completion : ( Result < Data , Error > ) -> Void = { [ weak self] result in
68+ guard let self else { return }
69+ switch result {
70+ case . success( let data) :
71+ self . prepareFeaturesData ( data: data)
72+ case . failure( let error) :
73+ logger. error ( " Failed to fetch features: \( error. localizedDescription) " )
74+ self . notify { $0. featuresFetchFailed ( error: . failedToLoadData, isRemote: true ) }
7375 }
76+ }
77+
78+ if remoteEval {
79+ dataSource. fetchRemoteEval ( apiUrl: apiUrl, params: payload, fetchResult: completion)
7480 } else {
75- delegate? . featuresFetchFailed ( error: . failedToLoadData, isRemote: false )
76- if logging { logger. info ( " Cache directory is empty. Nothing to fetch. " ) }
81+ dataSource. fetchFeatures ( apiUrl: apiUrl, fetchResult: completion)
7782 }
7883 }
79-
80- /// Fetch Features
81- func fetchFeatures( apiUrl: String ? , remoteEval: Bool = false , payload: RemoteEvalParams ? = nil ) {
82- // Check for cache data
83- fetchCachedFeatures ( logging: true )
8484
85- if let apiUrl = apiUrl {
86- if remoteEval {
87- dataSource. fetchRemoteEval ( apiUrl: apiUrl, params: payload) { result in
88- switch result {
89- case . success( let data) :
90- self . prepareFeaturesData ( data: data)
91- case . failure( let error) :
92- self . delegate? . featuresFetchFailed ( error: . failedToLoadData, isRemote: true )
93- logger. error ( " Failed get features: \( error. localizedDescription) " )
94- }
95- }
85+ private func fetchCachedFeatures( logging: Bool = false ) {
86+ guard let data = manager. getContent ( fileName: Constants . featureCache) else {
87+ if logging { logger. info ( " Cache directory is empty. Nothing to fetch. " ) }
88+ notify { $0. featuresFetchFailed ( error: . failedToLoadData, isRemote: false ) }
89+ return
90+ }
91+
92+ let decoder = JSONDecoder ( )
93+
94+ if let encryptedString = String ( data: data, encoding: . utf8) ,
95+ let encryptionKey, !encryptionKey. isEmpty {
96+
97+ let crypto : CryptoProtocol = Crypto ( )
98+ if let features = crypto. getFeaturesFromEncryptedFeatures ( encryptedString: encryptedString, encryptionKey: encryptionKey) {
99+ notify { $0. featuresFetchedSuccessfully ( features: features, isRemote: false ) }
96100 } else {
97- dataSource. fetchFeatures ( apiUrl: apiUrl) { result in
98- switch result {
99- case . success( let data) :
100- self . prepareFeaturesData ( data: data)
101- case . failure( let error) :
102- self . delegate? . featuresFetchFailed ( error: . failedToLoadData, isRemote: true )
103- logger. error ( " Failed get features: \( error. localizedDescription) " )
104- }
105- }
101+ if logging { logger. error ( " Failed get features from cached encrypted features " ) }
102+ notify { $0. featuresFetchFailed ( error: . failedParsedEncryptedData, isRemote: false ) }
106103 }
104+
105+ } else if let features = try ? decoder. decode ( Features . self, from: data) {
106+ notify { $0. featuresFetchedSuccessfully ( features: features, isRemote: false ) }
107107 } else {
108- delegate ? . featuresFetchFailed ( error: . failedMissingKey , isRemote : true )
109- logger . error ( " Failed get api URL " )
108+ if logging { logger . error ( " Failed to parse local data " ) }
109+ notify { $0 . featuresFetchFailed ( error : . failedParsedData , isRemote : false ) }
110110 }
111111 }
112-
113- /// Cache API Response and push success event
114- func prepareFeaturesData( data: Data ) {
115- // Call Success Delegate with mention of data available with remote
116112
113+ func prepareFeaturesData( data: Data ) {
117114 let decoder = JSONDecoder ( )
118- if let jsonPetitions = try ? decoder. decode ( FeaturesDataModel . self, from: data) {
119- delegate? . featuresAPIModelSuccessfully ( model: jsonPetitions)
120- if let encryptedString = jsonPetitions. encryptedFeatures {
121- if let encryptionKey = encryptionKey, !encryptionKey. isEmpty {
122- let crypto : CryptoProtocol = Crypto ( )
123- if let features = crypto. getFeaturesFromEncryptedFeatures ( encryptedString: encryptedString, encryptionKey: encryptionKey) {
124- if let featureData = encryptedString. data ( using: . utf8) {
125- manager. saveContent ( fileName: Constants . featureCache, content: featureData)
126- } else {
127- logger. error ( " Failed encode features " )
128- }
129- delegate? . featuresFetchedSuccessfully ( features: features, isRemote: true )
130- } else {
131- delegate? . featuresFetchFailed ( error: . failedEncryptedFeatures, isRemote: true )
132- logger. error ( " Failed get features from encrypted features " )
133- return
134- }
135- } else {
136- delegate? . featuresFetchFailed ( error: . failedMissingKey, isRemote: true )
137- logger. error ( " Failed get encryption key or it's empty " )
138- return
139- }
140- } else if let features = jsonPetitions. features {
141- if let featureData = try ? JSONEncoder ( ) . encode ( features) {
142- manager. saveContent ( fileName: Constants . featureCache, content: featureData)
115+
116+ guard let jsonPetitions = try ? decoder. decode ( FeaturesDataModel . self, from: data) else {
117+ logger. error ( " Failed to decode FeaturesDataModel " )
118+ notify { $0. featuresFetchFailed ( error: . failedParsedData, isRemote: true ) }
119+ return
120+ }
121+
122+ if let encryptedString = jsonPetitions. encryptedFeatures {
123+ handleEncryptedFeatures ( encryptedString: encryptedString, jsonPetitions: jsonPetitions)
124+ } else if let features = jsonPetitions. features {
125+ handlePlainFeatures ( features, jsonPetitions: jsonPetitions)
126+ } else {
127+ logger. error ( " Missing both encrypted and plain features " )
128+ notify { $0. featuresFetchFailed ( error: . failedMissingKey, isRemote: true ) }
129+ }
130+ }
131+
132+ private func handleEncryptedFeatures( encryptedString: String , jsonPetitions: FeaturesDataModel ) {
133+ guard let encryptionKey = encryptionKey, !encryptionKey. isEmpty else {
134+ logger. error ( " Missing encryption key " )
135+ notify { $0. featuresFetchFailed ( error: . failedMissingKey, isRemote: true ) }
136+ return
137+ }
138+
139+ let crypto = Crypto ( )
140+ guard let features = crypto. getFeaturesFromEncryptedFeatures ( encryptedString: encryptedString, encryptionKey: encryptionKey) else {
141+ logger. error ( " Failed to decrypt features " )
142+ notify { $0. featuresFetchFailed ( error: . failedEncryptedFeatures, isRemote: true ) }
143+ return
144+ }
145+
146+ if let featureData = encryptedString. data ( using: . utf8) {
147+ saveDataThreadSafe ( fileName: Constants . featureCache, content: featureData)
148+ } else {
149+ logger. error ( " Failed to encode features as UTF-8 " )
150+ }
151+
152+ notify { $0. featuresFetchedSuccessfully ( features: features, isRemote: true ) }
153+ handleSavedGroups ( from: jsonPetitions)
154+ }
155+
156+ private func handlePlainFeatures( _ features: Features , jsonPetitions: FeaturesDataModel ) {
157+ if let featureData = try ? JSONEncoder ( ) . encode ( features) {
158+ saveDataThreadSafe ( fileName: Constants . featureCache, content: featureData)
159+ }
160+
161+ notify { $0. featuresFetchedSuccessfully ( features: features, isRemote: true ) }
162+ handleSavedGroups ( from: jsonPetitions)
163+ }
164+
165+ private func handleSavedGroups( from jsonPetitions: FeaturesDataModel ) {
166+ if let encryptedSavedGroups = jsonPetitions. encryptedSavedGroups,
167+ !encryptedSavedGroups. isEmpty,
168+ let encryptionKey = encryptionKey,
169+ !encryptionKey. isEmpty {
170+
171+ let crypto = Crypto ( )
172+ if let savedGroups = crypto. getSavedGroupsFromEncryptedFeatures ( encryptedString: encryptedSavedGroups, encryptionKey: encryptionKey) {
173+ if let savedGroupsData = encryptedSavedGroups. data ( using: . utf8) {
174+ saveDataThreadSafe ( fileName: Constants . savedGroupsCache, content: savedGroupsData)
143175 }
144- delegate ? . featuresFetchedSuccessfully ( features : features , isRemote: true )
176+ notify { $0 . savedGroupsFetchedSuccessfully ( savedGroups : savedGroups , isRemote: true ) }
145177 } else {
146- delegate? . featuresFetchFailed ( error: . failedMissingKey, isRemote: true )
147- logger. error ( " Failed get encrypted features or it's empty " )
148- return
178+ logger. error ( " Failed to decrypt saved groups " )
179+ notify { $0. savedGroupsFetchFailed ( error: . failedEncryptedSavedGroups, isRemote: true ) }
149180 }
150181
151- if let encryptedSavedGroups = jsonPetitions. encryptedSavedGroups, !encryptedSavedGroups. isEmpty, let encryptionKey = encryptionKey, !encryptionKey. isEmpty {
152- let crypto = Crypto ( )
153- if let savedGroups = crypto. getSavedGroupsFromEncryptedFeatures ( encryptedString: encryptedSavedGroups, encryptionKey: encryptionKey) {
154- if let encryptedSavedGroups = encryptedSavedGroups. data ( using: . utf8) {
155- manager. saveContent ( fileName: Constants . savedGroupsCache, content: encryptedSavedGroups)
156- } else {
157- logger. error ( " Failed encode saved groups " )
158- }
159- delegate? . savedGroupsFetchedSuccessfully ( savedGroups: savedGroups, isRemote: true )
160- } else {
161- delegate? . savedGroupsFetchFailed ( error: . failedEncryptedSavedGroups, isRemote: true )
162- logger. error ( " Failed get saved groups from encrypted saved groups " )
163- return
164- }
165- } else if let savedGroups = jsonPetitions. savedGroups {
166- if let savedGroupsData = try ? JSONEncoder ( ) . encode ( savedGroups) {
167- manager. saveContent ( fileName: Constants . savedGroupsCache, content: savedGroupsData)
168- }
169- delegate? . savedGroupsFetchedSuccessfully ( savedGroups: savedGroups, isRemote: true )
182+ } else if let savedGroups = jsonPetitions. savedGroups {
183+ if let savedGroupsData = try ? JSONEncoder ( ) . encode ( savedGroups) {
184+ saveDataThreadSafe ( fileName: Constants . savedGroupsCache, content: savedGroupsData)
170185 }
171- } else {
172- delegate? . featuresFetchFailed ( error: . failedParsedData, isRemote: true )
173- logger. error ( " Failed get features data model " )
186+ notify { $0. savedGroupsFetchedSuccessfully ( savedGroups: savedGroups, isRemote: true ) }
187+ }
188+ }
189+
190+ private func saveDataThreadSafe( fileName: String , content: Data ) {
191+ if forceSynchronousSave {
192+ manager. saveContent ( fileName: fileName, content: content)
174193 return
194+ } else {
195+ fileSaveQueue. async { [ weak self] in
196+ self ? . manager. saveContent ( fileName: fileName, content: content)
197+ }
175198 }
176199 }
177200
201+ private func notify( _ action: @escaping ( FeaturesFlowDelegate ) -> Void ) {
202+ if Thread . isMainThread {
203+ guard let delegate = self . delegate else { return }
204+ action ( delegate)
205+ } else {
206+ DispatchQueue . main. async { [ weak self] in
207+ guard let self, let delegate = self . delegate else { return }
208+ action ( delegate)
209+ }
210+ }
211+ }
178212}
0 commit comments