|
|
|
|
@ -19,6 +19,7 @@ public class StoreCenter { |
|
|
|
|
/// A dictionary of Stores associated to their id |
|
|
|
|
fileprivate var _stores: [String: Store] = [:] |
|
|
|
|
|
|
|
|
|
/// Returns a default Store instance |
|
|
|
|
lazy var mainStore: Store = { Store(storeCenter: self) }() |
|
|
|
|
|
|
|
|
|
/// A KeychainStore object used to store the user's token |
|
|
|
|
@ -61,6 +62,7 @@ public class StoreCenter { |
|
|
|
|
/// The URL manager |
|
|
|
|
fileprivate var _urlManager: URLManager? = nil |
|
|
|
|
|
|
|
|
|
/// Used for testing, gives the project name to retrieve classes from names |
|
|
|
|
var classProject: String? = nil |
|
|
|
|
|
|
|
|
|
init(directoryName: String? = nil) { |
|
|
|
|
@ -72,6 +74,11 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
self.loadApiCallCollection(type: GetSyncData.self) |
|
|
|
|
|
|
|
|
|
if let directoryName { |
|
|
|
|
self._settingsStorage = MicroStorage( |
|
|
|
|
fileName: "\(directoryName)/settings.json") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
NetworkMonitor.shared.onConnectionEstablished = { |
|
|
|
|
self._resumeApiCalls() |
|
|
|
|
// self._configureWebSocket() |
|
|
|
|
@ -442,7 +449,7 @@ public class StoreCenter { |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
func executeGet<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
func executeGet<T: SyncedStorable>(apiCall: ApiCall<T>) async throws -> Data { |
|
|
|
|
return try await self.service().runGetApiCall(apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -563,7 +570,7 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func testSynchronizeOnceAsync() async throws { |
|
|
|
|
func testSynchronizeOnceAsync() async throws -> Data { |
|
|
|
|
guard self.isAuthenticated else { |
|
|
|
|
throw StoreError.missingToken |
|
|
|
|
} |
|
|
|
|
@ -572,7 +579,7 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
let getSyncData = GetSyncData() |
|
|
|
|
getSyncData.date = lastSync |
|
|
|
|
await syncGetCollection.executeSingleGet(instance: getSyncData) |
|
|
|
|
return try await syncGetCollection.executeSingleGet(instance: getSyncData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func sendGetRequest<T: SyncedStorable>(_ type: T.Type, storeId: String?, clear: Bool) async throws { |
|
|
|
|
@ -599,69 +606,33 @@ public class StoreCenter { |
|
|
|
|
Logger.w("data unrecognized: \(string)") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
try await self._parseSyncUpdates(json, shared: true) |
|
|
|
|
|
|
|
|
|
let array = try self.decodeDictionary(json) |
|
|
|
|
await self._syncAddOrUpdate(array, shared: true) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Processes the data coming from a sync request |
|
|
|
|
@MainActor func synchronizeContent(_ data: Data) { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
guard |
|
|
|
|
let json = try JSONSerialization.jsonObject(with: data, options: []) |
|
|
|
|
as? [String: Any] |
|
|
|
|
else { |
|
|
|
|
Logger.w("data unrecognized") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let updates = json["updates"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(updates) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let deletions = json["deletions"] as? [String: Any] { |
|
|
|
|
try self._parseSyncDeletions(deletions) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let updates = json["grants"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(updates, shared: true) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let revocations = json["revocations"] as? [String: Any] { |
|
|
|
|
try self._parseSyncRevocations(revocations, parents: json["revocation_parents"] as? [[String: Any]]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Data access events |
|
|
|
|
if let rs = json["relationship_sets"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(rs) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let rr = json["relationship_removals"] as? [String: Any] { |
|
|
|
|
try self._parseSyncDeletions(rr) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let srs = json["shared_relationship_sets"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(srs, shared: true) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let srm = json["shared_relationship_removals"] as? [String: Any] { |
|
|
|
|
self._synchronizationRevoke(items: srm) |
|
|
|
|
@MainActor func synchronizeContent(_ syncData: SyncData) { |
|
|
|
|
|
|
|
|
|
self._syncAddOrUpdate(syncData.updates) |
|
|
|
|
self._syncDelete(syncData.deletions) |
|
|
|
|
self._syncAddOrUpdate(syncData.grants, shared: true) |
|
|
|
|
self.syncRevoke(syncData.revocations, parents: syncData.revocationParents) |
|
|
|
|
self._syncAddOrUpdate(syncData.relationshipSets) |
|
|
|
|
self._syncDelete(syncData.relationshipRemovals) |
|
|
|
|
self._syncAddOrUpdate(syncData.sharedRelationshipSets) |
|
|
|
|
self._syncRevoke(syncData.sharedRelationshipRemovals) |
|
|
|
|
|
|
|
|
|
if let dateString = syncData.date { |
|
|
|
|
Logger.log("Sets sync date = \(dateString)") |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.lastSynchronization = dateString |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let dateString = json["date"] as? String { |
|
|
|
|
Logger.log("Sets sync date = \(dateString)") |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.lastSynchronization = dateString |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} catch { |
|
|
|
|
self.log(message: error.localizedDescription) |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
name: NSNotification.Name.LeStorageDidSynchronize, object: self) |
|
|
|
|
|
|
|
|
|
@ -669,99 +640,50 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
/// Processes data that should be inserted or updated inside the app |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - updates: the server updates |
|
|
|
|
/// - updateArrays: the server updates |
|
|
|
|
/// - shared: indicates if the content should be flagged as shared |
|
|
|
|
@MainActor func _parseSyncUpdates(_ updates: [String: Any], shared: Bool = false) throws { |
|
|
|
|
for (className, updateData) in updates { |
|
|
|
|
|
|
|
|
|
guard let updateArray = updateData as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
Logger.log(">>> UPDATE \(updateArray.count) \(className)") |
|
|
|
|
|
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
|
|
|
|
|
for updateItem in updateArray { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let jsonData = try JSONSerialization.data( |
|
|
|
|
withJSONObject: updateItem, options: []) |
|
|
|
|
let decodedObject = try JSON.decoder.decode(type, from: jsonData) |
|
|
|
|
// Logger.log(">>> \(decodedObject.lastUpdate.timeIntervalSince1970) : \(decodedObject.id)") |
|
|
|
|
|
|
|
|
|
let storeId: String? = decodedObject.getStoreId() |
|
|
|
|
self.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) |
|
|
|
|
} catch { |
|
|
|
|
Logger.w("Issue with json decoding: \(updateItem)") |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
@MainActor func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: Bool = false) { |
|
|
|
|
|
|
|
|
|
for updateArray in updateArrays { |
|
|
|
|
for item in updateArray.items { |
|
|
|
|
let storeId: String? = item.getStoreId() |
|
|
|
|
self.synchronizationAddOrUpdate(item, storeId: storeId, shared: shared) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Processes data that should be deleted inside the app |
|
|
|
|
fileprivate func _parseSyncDeletions(_ deletions: [String: Any]) throws { |
|
|
|
|
for (className, deleteData) in deletions { |
|
|
|
|
guard let deletedItems = deleteData as? [Any] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for deleted in deletedItems { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let data = try JSONSerialization.data(withJSONObject: deleted, options: []) |
|
|
|
|
let deletedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) |
|
|
|
|
|
|
|
|
|
self.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _syncDelete(_ deletionArrays: [ObjectIdentifierArray]) { |
|
|
|
|
|
|
|
|
|
for deletionArray in deletionArrays { |
|
|
|
|
for deletedObject in deletionArray.items { |
|
|
|
|
self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Processes data that has been revoked |
|
|
|
|
fileprivate func _parseSyncRevocations(_ deletions: [String: Any], parents: [[String: Any]]?) throws { |
|
|
|
|
for (className, revocationData) in deletions { |
|
|
|
|
guard let revokedItems = revocationData as? [Any] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
for revoked in revokedItems { |
|
|
|
|
do { |
|
|
|
|
let data = try JSONSerialization.data(withJSONObject: revoked, options: []) |
|
|
|
|
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) |
|
|
|
|
self.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
fileprivate func syncRevoke(_ revokedArrays: [ObjectIdentifierArray], parents: [[ObjectIdentifierArray]]) { |
|
|
|
|
|
|
|
|
|
self._syncRevoke(revokedArrays) |
|
|
|
|
for revokedArray in revokedArrays { |
|
|
|
|
for revoked in revokedArray.items { |
|
|
|
|
self.synchronizationDelete(id: revoked.modelId, type: revokedArray.type, storeId: revoked.storeId) // or synchronizationRevoke ? |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let parents { |
|
|
|
|
for level in parents { |
|
|
|
|
self._synchronizationRevoke(items: level) |
|
|
|
|
} |
|
|
|
|
for level in parents { |
|
|
|
|
self._syncRevoke(level) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _synchronizationRevoke(items: [String: Any]) { |
|
|
|
|
for (className, parentData) in items { |
|
|
|
|
guard let parentItems = parentData as? [Any] else { |
|
|
|
|
Logger.w("Invalid update data for \(className): \(parentData)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
for parentItem in parentItems { |
|
|
|
|
do { |
|
|
|
|
let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) |
|
|
|
|
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) |
|
|
|
|
self.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
fileprivate func _syncRevoke(_ revokeArrays: [ObjectIdentifierArray]) { |
|
|
|
|
|
|
|
|
|
for revokeArray in revokeArrays { |
|
|
|
|
for revoked in revokeArray.items { |
|
|
|
|
self.synchronizationRevoke(id: revoked.modelId, type: revokeArray.type, storeId: revoked.storeId) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -814,11 +736,11 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes an instance with the given parameters |
|
|
|
|
func synchronizationDelete(id: String, model: String, storeId: String?) { |
|
|
|
|
func synchronizationDelete<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
// let type = try self.classFromName(model) |
|
|
|
|
try self._store(id: storeId).deleteNoSync(type: type, id: id) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
@ -828,11 +750,11 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Revokes a data that has been shared with the user |
|
|
|
|
func synchronizationRevoke(id: String, model: String, storeId: String?) { |
|
|
|
|
func synchronizationRevoke<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
// let type = try self.classFromName(model) |
|
|
|
|
if self._instanceShared(id: id, type: type) { |
|
|
|
|
let count = self.mainStore.referenceCount(type: type, id: id) |
|
|
|
|
if count == 0 { |
|
|
|
|
@ -870,6 +792,49 @@ public class StoreCenter { |
|
|
|
|
self._deleteLogs.addOrUpdate(instance: dataLog) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Sync data conversion |
|
|
|
|
|
|
|
|
|
func decodeObjectIdentifierDictionary(_ dictionary: [String: Any]) throws -> [ObjectIdentifierArray] { |
|
|
|
|
|
|
|
|
|
var objectIdentifierArray: [ObjectIdentifierArray] = [] |
|
|
|
|
|
|
|
|
|
for (className, dataArray) in dictionary { |
|
|
|
|
|
|
|
|
|
guard let array = dataArray as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
let decodedArray = try self._decodeArray(type: ObjectIdentifier.self, array: array) |
|
|
|
|
objectIdentifierArray.append(ObjectIdentifierArray(type: type, items: decodedArray)) |
|
|
|
|
} |
|
|
|
|
return objectIdentifierArray |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func decodeDictionary(_ dictionary: [String: Any]) throws -> [SyncedStorableArray] { |
|
|
|
|
|
|
|
|
|
var syncedStorableArray: [SyncedStorableArray] = [] |
|
|
|
|
|
|
|
|
|
for (className, dataArray) in dictionary { |
|
|
|
|
|
|
|
|
|
guard let array = dataArray as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
Logger.log(">>> UPDATE \(array.count) \(className)") |
|
|
|
|
|
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
let decodedArray = try self._decodeArray(type: type, array: array) |
|
|
|
|
syncedStorableArray.append(SyncedStorableArray(type: type, items: decodedArray)) |
|
|
|
|
} |
|
|
|
|
return syncedStorableArray |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _decodeArray<T: Decodable>(type: T.Type, array: [[String : Any]]) throws -> [T] { |
|
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: array, options: []) |
|
|
|
|
return try JSON.decoder.decode([T].self, from: jsonData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Miscellanous |
|
|
|
|
|
|
|
|
|
/// Returns the count of api calls for a Type |
|
|
|
|
|