Refactor and cleanup for tests

sync3
Laurent 6 months ago
parent 369c71ba4e
commit e55f183053
  1. 4
      LeStorage.xcodeproj/project.pbxproj
  2. 27
      LeStorage/ApiCallCollection.swift
  3. 79
      LeStorage/Codables/SyncData.swift
  4. 31
      LeStorage/Services.swift
  5. 227
      LeStorage/StoreCenter.swift

@ -20,6 +20,7 @@
C471F2582DB10649006317F4 /* MockKeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471F2572DB10649006317F4 /* MockKeychainStore.swift */; }; C471F2582DB10649006317F4 /* MockKeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471F2572DB10649006317F4 /* MockKeychainStore.swift */; };
C48638B32D9BC6A8007E3E06 /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */; }; C48638B32D9BC6A8007E3E06 /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */; };
C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C87F2CCBDC210082001F /* NetworkMonitor.swift */; }; C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C87F2CCBDC210082001F /* NetworkMonitor.swift */; };
C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49774DE2DC4B3D7005CD239 /* SyncData.swift */; };
C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; }; C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; };
C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; }; C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; };
C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */; }; C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */; };
@ -77,6 +78,7 @@
C471F2572DB10649006317F4 /* MockKeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychainStore.swift; sourceTree = "<group>"; }; C471F2572DB10649006317F4 /* MockKeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychainStore.swift; sourceTree = "<group>"; };
C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = "<group>"; }; C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = "<group>"; };
C488C87F2CCBDC210082001F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; }; C488C87F2CCBDC210082001F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
C49774DE2DC4B3D7005CD239 /* SyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncData.swift; sourceTree = "<group>"; };
C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = "<group>"; }; C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = "<group>"; };
C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = "<group>"; }; C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = "<group>"; };
C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollection.swift; sourceTree = "<group>"; }; C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollection.swift; sourceTree = "<group>"; };
@ -219,6 +221,7 @@
C400D7222CC2AF560092237C /* GetSyncData.swift */, C400D7222CC2AF560092237C /* GetSyncData.swift */,
C4AC9CE42CEFB12100CC13DF /* DataAccess.swift */, C4AC9CE42CEFB12100CC13DF /* DataAccess.swift */,
C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */, C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */,
C49774DE2DC4B3D7005CD239 /* SyncData.swift */,
); );
path = Codables; path = Codables;
sourceTree = "<group>"; sourceTree = "<group>";
@ -363,6 +366,7 @@
C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */, C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */, C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */,
C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */, C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */,
C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */,
C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */, C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */,
C462E0DC2D37B61100F3E6E4 /* Notification+Name.swift in Sources */, C462E0DC2D37B61100F3E6E4 /* Notification+Name.swift in Sources */,
C4A47D812B7665AD00ADC637 /* Migration.swift in Sources */, C4A47D812B7665AD00ADC637 /* Migration.swift in Sources */,

@ -236,9 +236,9 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
if batch.count == 1, let apiCall = batch.first, apiCall.method == .get { if batch.count == 1, let apiCall = batch.first, apiCall.method == .get {
try await self._executeGetCall(apiCall: apiCall) try await self._executeGetCall(apiCall: apiCall)
} else { } else {
let results = try await self._executeApiCalls(batch) let results: [OperationResult<T>] = try await self._executeApiCalls(batch)
if T.copyServerResponse { if T.copyServerResponse {
let instances = results.compactMap { $0.data } let instances: [T] = results.compactMap { $0.data }
self.storeCenter.updateLocalInstances(instances) self.storeCenter.updateLocalInstances(instances)
} }
} }
@ -248,13 +248,26 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
} }
} }
fileprivate func _executeGetCall(apiCall: ApiCall<T>) async throws { @discardableResult func _executeGetCall(apiCall: ApiCall<T>) async throws -> Data {
let data = try await self.storeCenter.executeGet(apiCall: apiCall)
if T.self == GetSyncData.self { if T.self == GetSyncData.self {
let _: Empty = try await self.storeCenter.executeGet(apiCall: apiCall) let syncData = try SyncData(data: data, storeCenter: self.storeCenter)
await self.storeCenter.synchronizeContent(syncData)
} else { } else {
let results: [T] = try await self.storeCenter.executeGet(apiCall: apiCall) let results: [T] = try self._decode(data: data)
await self.storeCenter.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive) await self.storeCenter.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive)
} }
return data
}
fileprivate func _decode<V: Decodable>(data: Data) throws -> V {
if !(V.self is Empty?.Type || V.self is Empty.Type) {
return try JSON.decoder.decode(V.self, from: data)
} else {
return try JSON.decoder.decode(V.self, from: "{}".data(using: .utf8)!)
}
} }
/// Wait for an exponentionnaly long time depending on the number of attemps /// Wait for an exponentionnaly long time depending on the number of attemps
@ -376,11 +389,11 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
await self._batchExecution() await self._batchExecution()
} }
func executeSingleGet(instance: T) async where T : URLParameterConvertible { func executeSingleGet(instance: T) async throws -> Data where T : URLParameterConvertible {
let call = self._createCall(.get, instance: instance, option: .none) let call = self._createCall(.get, instance: instance, option: .none)
call.urlParameters = instance.queryParameters(storeCenter: self.storeCenter) call.urlParameters = instance.queryParameters(storeCenter: self.storeCenter)
self._addCallToWaitingList(call) self._addCallToWaitingList(call)
await self._batchExecution() return try await self._executeGetCall(apiCall: call)
} }
fileprivate func _prepareCalls(batch: OperationBatch<T>) { fileprivate func _prepareCalls(batch: OperationBatch<T>) {

@ -0,0 +1,79 @@
//
// SyncData.swift
// LeStorage
//
// Created by Laurent Morvillier on 02/05/2025.
//
import Foundation
enum SyncDataError: Error {
case invalidFormat
}
struct SyncedStorableArray {
var type: any SyncedStorable.Type
var items: [any SyncedStorable]
}
struct ObjectIdentifierArray {
var type: any SyncedStorable.Type
var items: [ObjectIdentifier]
}
class SyncData {
var updates: [SyncedStorableArray] = []
var deletions: [ObjectIdentifierArray] = []
var grants: [SyncedStorableArray] = []
var revocations: [ObjectIdentifierArray] = []
var revocationParents: [[ObjectIdentifierArray]] = []
var relationshipSets: [SyncedStorableArray] = []
var relationshipRemovals: [ObjectIdentifierArray] = []
var sharedRelationshipSets: [SyncedStorableArray] = []
var sharedRelationshipRemovals: [ObjectIdentifierArray] = []
var date: String?
init(data: Data, storeCenter: StoreCenter) throws {
guard let json = try JSONSerialization.jsonObject(with: data, options: [])
as? [String : Any]
else {
throw SyncDataError.invalidFormat
}
if let updates = json["updates"] as? [String: Any] {
self.updates = try storeCenter.decodeDictionary(updates)
}
if let deletions = json["deletions"] as? [String: Any] {
self.deletions = try storeCenter.decodeObjectIdentifierDictionary(deletions)
}
if let grants = json["grants"] as? [String: Any] {
self.grants = try storeCenter.decodeDictionary(grants)
}
if let revocations = json["revocations"] as? [String: Any] {
self.revocations = try storeCenter.decodeObjectIdentifierDictionary(revocations)
}
if let revocationParents = json["revocation_parents"] as? [[String: Any]] {
for level in revocationParents {
let decodedLevel = try storeCenter.decodeObjectIdentifierDictionary(level)
self.revocationParents.append(decodedLevel)
}
}
if let relationshipSets = json["relationship_sets"] as? [String: Any] {
self.relationshipSets = try storeCenter.decodeDictionary(relationshipSets)
}
if let relationshipRemovals = json["relationship_removals"] as? [String: Any] {
self.relationshipRemovals = try storeCenter.decodeObjectIdentifierDictionary(relationshipRemovals)
}
if let sharedRelationshipSets = json["shared_relationship_sets"] as? [String: Any] {
self.sharedRelationshipSets = try storeCenter.decodeDictionary(sharedRelationshipSets)
}
if let sharedRelationshipRemovals = json["shared_relationship_removals"] as? [String: Any] {
self.sharedRelationshipRemovals = try storeCenter.decodeObjectIdentifierDictionary(sharedRelationshipRemovals)
}
self.date = json["date"] as? String
}
}

@ -81,9 +81,9 @@ public class Services {
/// - Parameters: /// - Parameters:
/// - request: the URLRequest to run /// - request: the URLRequest to run
/// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure
fileprivate func _runGetApiCallRequest<T: SyncedStorable, V: Decodable>( fileprivate func _runGetApiCallRequest<T: SyncedStorable>(
_ request: URLRequest, apiCall: ApiCall<T> _ request: URLRequest, apiCall: ApiCall<T>
) async throws -> V { ) async throws -> Data {
let debugURL = request.url?.absoluteString ?? "" let debugURL = request.url?.absoluteString ?? ""
// print("Run \(request.httpMethod ?? "") \(debugURL)") // print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
@ -95,11 +95,6 @@ public class Services {
switch statusCode { switch statusCode {
case 200..<300: // success case 200..<300: // success
try await self.storeCenter.deleteApiCallById(type: T.self, id: apiCall.id) try await self.storeCenter.deleteApiCallById(type: T.self, id: apiCall.id)
if T.self == GetSyncData.self {
await self.storeCenter.synchronizeContent(task.0)
}
default: // error default: // error
Logger.log( Logger.log(
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") "Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")")
@ -123,7 +118,7 @@ public class Services {
Logger.w(message) Logger.w(message)
} }
return try self._decode(data: task.0) return task.0 //try self._decode(data: task.0)
} }
@ -420,15 +415,15 @@ public class Services {
return request return request
} }
/// Starts a request to retrieve the synchronization updates // /// Starts a request to retrieve the synchronization updates
/// - Parameters: // /// - Parameters:
/// - since: The date from which updates are retrieved // /// - since: The date from which updates are retrieved
func synchronizeLastUpdates(since: Date?) async throws { // func synchronizeLastUpdates(since: Date?) async throws {
let request = try self._getSyncLogRequest(since: since) // let request = try self._getSyncLogRequest(since: since)
if let data = try await self._runRequest(request) { // if let data = try await self._runRequest(request) {
await self.storeCenter.synchronizeContent(data) // await self.storeCenter.synchronizeContent(data)
} // }
} // }
/// Returns the URLRequest for an ApiCall /// Returns the URLRequest for an ApiCall
/// - Parameters: /// - Parameters:
@ -520,7 +515,7 @@ public class Services {
} }
/// Executes an ApiCall /// Executes an ApiCall
func runGetApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { func runGetApiCall<T: SyncedStorable>(_ apiCall: ApiCall<T>) async throws -> Data {
let request = try self._syncGetRequest(from: apiCall) let request = try self._syncGetRequest(from: apiCall)
return try await self._runGetApiCallRequest(request, apiCall: apiCall) return try await self._runGetApiCallRequest(request, apiCall: apiCall)
} }

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

Loading…
Cancel
Save