|
|
|
|
@ -9,13 +9,13 @@ import Foundation |
|
|
|
|
import UIKit |
|
|
|
|
|
|
|
|
|
public class StoreCenter { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The main instance |
|
|
|
|
public static let main: StoreCenter = StoreCenter() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A dictionary of Stores associated to their id |
|
|
|
|
fileprivate var _stores: [String : Store] = [:] |
|
|
|
|
|
|
|
|
|
fileprivate var _stores: [String: Store] = [:] |
|
|
|
|
|
|
|
|
|
/// The URL of the django API |
|
|
|
|
public var synchronizationApiURL: String? { |
|
|
|
|
didSet { |
|
|
|
|
@ -24,39 +24,40 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Indicates to Stored Collection if they can synchronize |
|
|
|
|
public var collectionsCanSynchronize: Bool = true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Force the absence of synchronization |
|
|
|
|
public var forceNoSynchronization: Bool = false |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A store for the Settings object |
|
|
|
|
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(fileName: "settings.json") |
|
|
|
|
|
|
|
|
|
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage( |
|
|
|
|
fileName: "settings.json") |
|
|
|
|
|
|
|
|
|
/// The services performing the API calls |
|
|
|
|
fileprivate var _services: Services? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The dictionary of registered StoredCollections |
|
|
|
|
fileprivate var _apiCallCollections: [String : any SomeCallCollection] = [:] |
|
|
|
|
|
|
|
|
|
fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] |
|
|
|
|
|
|
|
|
|
/// A collection of DataLog objects, used for the synchronization |
|
|
|
|
fileprivate var _dataLogs: StoredCollection<DataLog> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A collection storing FailedAPICall objects |
|
|
|
|
fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A collection of Log objects |
|
|
|
|
fileprivate var _logs: StoredCollection<Log>? = nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A list of username that cannot synchronize with the server |
|
|
|
|
fileprivate var _blackListedUserName: [String] = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init() { |
|
|
|
|
self._dataLogs = Store.main.registerCollection() |
|
|
|
|
self._setupNotifications() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the service instance |
|
|
|
|
public func service() throws -> Services { |
|
|
|
|
if let service = self._services { |
|
|
|
|
@ -65,7 +66,7 @@ public class StoreCenter { |
|
|
|
|
throw StoreError.missingService |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func _setupNotifications() { |
|
|
|
|
NotificationCenter.default.addObserver( |
|
|
|
|
self, |
|
|
|
|
@ -78,9 +79,9 @@ public class StoreCenter { |
|
|
|
|
Logger.log("_willEnterForegroundNotification") |
|
|
|
|
self._launchSynchronization() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@objc fileprivate func _launchSynchronization() { |
|
|
|
|
Task{ |
|
|
|
|
Task { |
|
|
|
|
do { |
|
|
|
|
try await self.synchronizeLastUpdates() |
|
|
|
|
} catch { |
|
|
|
|
@ -101,7 +102,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
self._stores[identifier] = store |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a store using its identifier, and registers it if it does not exists |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The store identifer |
|
|
|
|
@ -115,9 +116,9 @@ public class StoreCenter { |
|
|
|
|
return store |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Settings |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Sets the user info given a user |
|
|
|
|
func setUserInfo(user: UserBase) { |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
@ -125,37 +126,37 @@ public class StoreCenter { |
|
|
|
|
settings.username = user.username |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the stored user Id |
|
|
|
|
public var userId: String? { |
|
|
|
|
return self._settingsStorage.item.userId |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the username |
|
|
|
|
public func userName() -> String? { |
|
|
|
|
return self._settingsStorage.item.username |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the stored token |
|
|
|
|
public func token() -> String? { |
|
|
|
|
return try? self.service().keychainStore.getValue() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Disconnect the user from the storage and resets collection |
|
|
|
|
public func disconnect() { |
|
|
|
|
try? self.service().deleteToken() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.resetApiCalls() |
|
|
|
|
self._failedAPICallsCollection?.reset() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.username = nil |
|
|
|
|
settings.userId = nil |
|
|
|
|
settings.lastSynchronization = nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns whether the system has a user token |
|
|
|
|
public func hasToken() -> Bool { |
|
|
|
|
do { |
|
|
|
|
@ -165,7 +166,7 @@ public class StoreCenter { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns a generated device id |
|
|
|
|
/// If created, stores it inside the keychain to get a consistent value even if the app is deleted |
|
|
|
|
/// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again |
|
|
|
|
@ -174,8 +175,8 @@ public class StoreCenter { |
|
|
|
|
do { |
|
|
|
|
return try keychainStore.getValue() |
|
|
|
|
} catch { |
|
|
|
|
let deviceId: String = UIDevice.current.identifierForVendor?.uuidString ?? |
|
|
|
|
UUID().uuidString |
|
|
|
|
let deviceId: String = |
|
|
|
|
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString |
|
|
|
|
do { |
|
|
|
|
try keychainStore.add(value: deviceId) |
|
|
|
|
} catch { |
|
|
|
|
@ -184,9 +185,9 @@ public class StoreCenter { |
|
|
|
|
return deviceId |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Api Calls management |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Instantiates and loads an ApiCallCollection with the provided type |
|
|
|
|
public func loadApiCallCollection<T: SyncedStorable>(type: T.Type) { |
|
|
|
|
if self._apiCallCollections[T.resourceName()] == nil { |
|
|
|
|
@ -201,7 +202,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the ApiCall collection using the resource name of the provided T type |
|
|
|
|
func apiCallCollection<T: Storable>() throws -> ApiCallCollection<T> { |
|
|
|
|
if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> { |
|
|
|
|
@ -218,7 +219,7 @@ public class StoreCenter { |
|
|
|
|
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() |
|
|
|
|
await apiCallCollection.deleteByDataId(id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Deletes an ApiCall by its id |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - type: the subsequent type of the ApiCall |
|
|
|
|
@ -227,7 +228,7 @@ public class StoreCenter { |
|
|
|
|
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() |
|
|
|
|
await apiCallCollection.deleteById(id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Deletes an ApiCall by its id |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - id: the id of the ApiCall |
|
|
|
|
@ -239,7 +240,7 @@ public class StoreCenter { |
|
|
|
|
throw StoreError.collectionNotRegistered(type: collectionName) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Resets all the api call collections |
|
|
|
|
public func resetApiCalls() { |
|
|
|
|
Task { |
|
|
|
|
@ -248,7 +249,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Resets the ApiCall whose type identifies with the provided collection |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - collection: The collection identifying the Storable type |
|
|
|
|
@ -262,9 +263,9 @@ public class StoreCenter { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Api call rescheduling |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Reschedule an ApiCall by id |
|
|
|
|
func rescheduleApiCalls<T: SyncedStorable>(id: String, type: T.Type) async throws { |
|
|
|
|
guard self.collectionsCanSynchronize else { |
|
|
|
|
@ -273,24 +274,27 @@ public class StoreCenter { |
|
|
|
|
let collection: ApiCallCollection<T> = try self.apiCallCollection() |
|
|
|
|
await collection.rescheduleApiCallsIfNecessary() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an ApiCall |
|
|
|
|
fileprivate func _executeApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
fileprivate func _executeApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) |
|
|
|
|
async throws -> V |
|
|
|
|
{ |
|
|
|
|
return try await self.service().runApiCall(apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
func execute<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
return try await self._executeApiCall(apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Api calls |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns whether the collection can synchronize |
|
|
|
|
fileprivate func _canSynchronise() -> Bool { |
|
|
|
|
return !self.forceNoSynchronization && self.collectionsCanSynchronize && self.userIsAllowed() |
|
|
|
|
return !self.forceNoSynchronization && self.collectionsCanSynchronize |
|
|
|
|
&& self.userIsAllowed() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Transmit the insertion request to the ApiCall collection |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - instance: an object to insert |
|
|
|
|
@ -300,7 +304,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
return try await self.apiCallCollection().sendInsertion(instance) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Transmit the update request to the ApiCall collection |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - instance: an object to update |
|
|
|
|
@ -310,7 +314,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
return try await self.apiCallCollection().sendUpdate(instance) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Transmit the deletion request to the ApiCall collection |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - instance: an object to delete |
|
|
|
|
@ -320,13 +324,17 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
try await self.apiCallCollection().sendDeletion(instance) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Retrieves all the items on the server |
|
|
|
|
func getItems<T: SyncedStorable>(identifier: StoreIdentifier? = nil) async throws -> [T] { |
|
|
|
|
return try await self.service().get(identifier: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Synchronization |
|
|
|
|
|
|
|
|
|
fileprivate func _createSyncApiCallCollection() { |
|
|
|
|
self.loadApiCallCollection(type: GetSyncData.self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func initialSynchronization() { |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
@ -334,16 +342,17 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
Store.main.loadCollectionsFromServer() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func synchronizeLastUpdates() async throws { |
|
|
|
|
let lastSync: Date? = self._settingsStorage.item.lastSynchronization |
|
|
|
|
try await self._services?.synchronizeLastUpdates(since: lastSync) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func synchronizeContent(_ data: Data, decoder: JSONDecoder) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
guard let json = try JSONSerialization.jsonObject(with: data, options: []) |
|
|
|
|
guard |
|
|
|
|
let json = try JSONSerialization.jsonObject(with: data, options: []) |
|
|
|
|
as? [String: Any] |
|
|
|
|
else { |
|
|
|
|
Logger.w("data unrecognized") |
|
|
|
|
@ -370,32 +379,32 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
if let dateString: String = json["date"] as? String, |
|
|
|
|
let syncDate = Date.iso8601Formatter.date(from: dateString) { |
|
|
|
|
|
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.lastSynchronization = syncDate |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _parseSyncUpdates(_ updates: [String: Any], decoder: JSONDecoder) throws { |
|
|
|
|
for (className, updateData) in updates { |
|
|
|
|
guard let updateArray = updateData as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let type = try self._classFromClassName(className) |
|
|
|
|
|
|
|
|
|
for updateItem in updateArray { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: updateItem, options: []) |
|
|
|
|
let jsonData = try JSONSerialization.data( |
|
|
|
|
withJSONObject: updateItem, options: []) |
|
|
|
|
let decodedObject = try decoder.decode(type, from: jsonData) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let storeId: String? = decodedObject.getStoreId() |
|
|
|
|
StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId) |
|
|
|
|
} catch { |
|
|
|
|
@ -404,25 +413,27 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _parseSyncDeletions(_ deletions: [String: Any], decoder: JSONDecoder) throws { |
|
|
|
|
for (className, updateDeletions) in deletions { |
|
|
|
|
guard let deletionArray = updateDeletions as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let type = try self._classFromClassName(className) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for updateItem in deletionArray { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let object = updateItem["data"] { |
|
|
|
|
do { |
|
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: object, options: []) |
|
|
|
|
let jsonData = try JSONSerialization.data( |
|
|
|
|
withJSONObject: object, options: []) |
|
|
|
|
let decodedObject = try decoder.decode(type, from: jsonData) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let storeId = updateItem["storeId"] as? String |
|
|
|
|
StoreCenter.main.synchronizationDelete(instance: decodedObject, storeId: storeId) |
|
|
|
|
StoreCenter.main.synchronizationDelete( |
|
|
|
|
instance: decodedObject, storeId: storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
@ -430,9 +441,9 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _classFromClassName(_ className: String) throws -> any SyncedStorable.Type { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let fullClassName = "PadelClub.\(className)" |
|
|
|
|
let modelClass: AnyClass? = NSClassFromString(fullClassName) |
|
|
|
|
if let type = modelClass as? any SyncedStorable.Type { |
|
|
|
|
@ -440,9 +451,9 @@ public class StoreCenter { |
|
|
|
|
} else { |
|
|
|
|
throw LeStorageError.cantFindClassFromName(name: className) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _store(id: String?) -> Store? { |
|
|
|
|
if let storeId = id { |
|
|
|
|
return self._stores[storeId] |
|
|
|
|
@ -450,47 +461,50 @@ public class StoreCenter { |
|
|
|
|
return Store.main |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool { |
|
|
|
|
return self._dataLogs.contains(where: { $0.dataId == instance.stringId && $0.operation == .delete }) |
|
|
|
|
return self._dataLogs.contains(where: { |
|
|
|
|
$0.dataId == instance.stringId && $0.operation == .delete |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?) { |
|
|
|
|
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) |
|
|
|
|
if !hasAlreadyBeenDeleted { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { |
|
|
|
|
self._store(id: storeId)?.addOrUpdateIfNewer(instance) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func synchronizationDelete<T: Storable>(instance: T, storeId: String?) { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
self._store(id: storeId)?.deleteNoSync(instance: instance) |
|
|
|
|
self._cleanupDataLog(dataId: instance.stringId) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _cleanupDataLog(dataId: String) { |
|
|
|
|
let logs = self._dataLogs.filter { $0.dataId == dataId } |
|
|
|
|
self._dataLogs.delete(contentOfs: logs) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// func createInsertLog<T: Storable>(_ instance: T) { |
|
|
|
|
// self._addDataLog(instance, method: .post) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
// func createInsertLog<T: Storable>(_ instance: T) { |
|
|
|
|
// self._addDataLog(instance, method: .post) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
func createDeleteLog<T: Storable>(_ instance: T) { |
|
|
|
|
self._addDataLog(instance, method: .delete) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _addDataLog<T: Storable>(_ instance: T, method: HTTPMethod) { |
|
|
|
|
let dataLog = DataLog(dataId: instance.stringId, modelName: String(describing: T.self), operation: method) |
|
|
|
|
let dataLog = DataLog( |
|
|
|
|
dataId: instance.stringId, modelName: String(describing: T.self), operation: method) |
|
|
|
|
self._dataLogs.addOrUpdate(instance: dataLog) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Miscellanous |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func apiCallCount<T: SyncedStorable>(type: T.Type) async -> Int { |
|
|
|
|
do { |
|
|
|
|
let collection: ApiCallCollection<T> = try self.apiCallCollection() |
|
|
|
|
@ -499,7 +513,7 @@ public class StoreCenter { |
|
|
|
|
return -1 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Resets all registered collection |
|
|
|
|
public func reset() { |
|
|
|
|
Store.main.reset() |
|
|
|
|
@ -507,7 +521,7 @@ public class StoreCenter { |
|
|
|
|
store.reset() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns whether any collection has pending API calls |
|
|
|
|
public func hasPendingAPICalls() async -> Bool { |
|
|
|
|
for collection in self._apiCallCollections.values { |
|
|
|
|
@ -522,33 +536,39 @@ public class StoreCenter { |
|
|
|
|
public func apiCallsFileContent(resourceName: String) async -> String { |
|
|
|
|
return await self._apiCallCollections[resourceName]?.contentOfFile() ?? "" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// This method triggers the framework to save and send failed api calls |
|
|
|
|
public func logsFailedAPICalls() { |
|
|
|
|
self._failedAPICallsCollection = Store.main.registerCollection() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// If configured for, logs and send to the server a failed API call |
|
|
|
|
/// Logs a failed API call that has failed at least 5 times |
|
|
|
|
func logFailedAPICall(_ apiCallId: String, request: URLRequest, collectionName: String, error: String) { |
|
|
|
|
|
|
|
|
|
func logFailedAPICall( |
|
|
|
|
_ apiCallId: String, request: URLRequest, collectionName: String, error: String |
|
|
|
|
) { |
|
|
|
|
|
|
|
|
|
guard let failedAPICallsCollection = self._failedAPICallsCollection, |
|
|
|
|
let collection = self._apiCallCollections[collectionName], |
|
|
|
|
collectionName != FailedAPICall.resourceName() |
|
|
|
|
else { |
|
|
|
|
let collection = self._apiCallCollections[collectionName], |
|
|
|
|
collectionName != FailedAPICall.resourceName() |
|
|
|
|
else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Task { |
|
|
|
|
if let apiCall = await collection.findCallById(apiCallId) { |
|
|
|
|
|
|
|
|
|
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 { |
|
|
|
|
|
|
|
|
|
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) |
|
|
|
|
&& apiCall.attemptsCount > 6 |
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let authValue = request.allHTTPHeaderFields?["Authorization"] |
|
|
|
|
let string = try apiCall.jsonString() |
|
|
|
|
let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error, authentication: authValue) |
|
|
|
|
|
|
|
|
|
let failedAPICall = FailedAPICall( |
|
|
|
|
callId: apiCall.id, type: collectionName, apiCall: string, error: error, |
|
|
|
|
authentication: authValue) |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
failedAPICallsCollection.addOrUpdate(instance: failedAPICall) |
|
|
|
|
} |
|
|
|
|
@ -558,25 +578,28 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Logs a failed Api call with its request and error message |
|
|
|
|
func logFailedAPICall(request: URLRequest, error: String) { |
|
|
|
|
|
|
|
|
|
guard let failedAPICallsCollection = self._failedAPICallsCollection, |
|
|
|
|
let body: Data = request.httpBody, |
|
|
|
|
let bodyString = String(data: body, encoding: .utf8), |
|
|
|
|
let url = request.url?.absoluteString else { |
|
|
|
|
let body: Data = request.httpBody, |
|
|
|
|
let bodyString = String(data: body, encoding: .utf8), |
|
|
|
|
let url = request.url?.absoluteString |
|
|
|
|
else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let authValue = request.allHTTPHeaderFields?["Authorization"] |
|
|
|
|
let failedAPICall = FailedAPICall(callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, authentication: authValue) |
|
|
|
|
let failedAPICall = FailedAPICall( |
|
|
|
|
callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, |
|
|
|
|
authentication: authValue) |
|
|
|
|
failedAPICallsCollection.addOrUpdate(instance: failedAPICall) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Adds a userName to the black list |
|
|
|
|
/// Black listed username cannot send data to the server |
|
|
|
|
/// - Parameters: |
|
|
|
|
@ -584,15 +607,15 @@ public class StoreCenter { |
|
|
|
|
public func blackListUserName(_ userName: String) { |
|
|
|
|
self._blackListedUserName.append(userName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns whether the current userName is allowed to sync with the server |
|
|
|
|
func userIsAllowed() -> Bool { |
|
|
|
|
guard let userName = self.userName() else { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
return !self._blackListedUserName.contains(where: { $0 == userName } ) |
|
|
|
|
return !self._blackListedUserName.contains(where: { $0 == userName }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Deletes the directory using its identifier |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The name of the directory |
|
|
|
|
@ -601,9 +624,9 @@ public class StoreCenter { |
|
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: directory) |
|
|
|
|
self._stores.removeValue(forKey: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Instant update |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Updates a local object with a server instance |
|
|
|
|
func updateFromServerInstance<T: SyncedStorable>(_ result: T) { |
|
|
|
|
if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) { |
|
|
|
|
@ -612,7 +635,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the collection hosting an instance |
|
|
|
|
func collectionOfInstance<T: Storable>(_ instance: T) -> StoredCollection<T>? { |
|
|
|
|
do { |
|
|
|
|
@ -626,7 +649,7 @@ public class StoreCenter { |
|
|
|
|
return self.collectionOfInstanceInSubStores(instance) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Search inside the additional stores to find the collection hosting the instance |
|
|
|
|
func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> StoredCollection<T>? { |
|
|
|
|
for store in self._stores.values { |
|
|
|
|
@ -637,9 +660,9 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Logs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the logs collection and instantiates it if necessary |
|
|
|
|
fileprivate func _logsCollection() -> StoredCollection<Log> { |
|
|
|
|
if let logs = self._logs { |
|
|
|
|
@ -650,15 +673,15 @@ public class StoreCenter { |
|
|
|
|
return logsCollection |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Logs a message in the logs collection |
|
|
|
|
public func log(message: String) { |
|
|
|
|
let log = Log(message: message) |
|
|
|
|
self._logsCollection().addOrUpdate(instance: log) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Migration |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Migrates the token from the provided service to the main Services instance |
|
|
|
|
public func migrateToken(_ services: Services) throws { |
|
|
|
|
guard let userName = self.userName() else { |
|
|
|
|
@ -666,9 +689,9 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
try self.service().migrateToken(services, userName: userName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deinit { |
|
|
|
|
NotificationCenter.default.removeObserver(self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|