From c8f204462ad414e17f2bb313f5c2f31ba7f8f79e Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 15 Apr 2025 15:28:17 +0200 Subject: [PATCH] Refactoring to pass a reference of StoreCenter in the various classes --- LeStorage/ApiCallCollection.swift | 20 +++++--- LeStorage/BaseCollection.swift | 10 ++-- LeStorage/Codables/GetSyncData.swift | 4 +- LeStorage/Services.swift | 59 +++++++++++---------- LeStorage/Store.swift | 20 +++++--- LeStorage/StoreCenter.swift | 60 ++++++++++++---------- LeStorage/SyncedCollection.swift | 26 +++++----- LeStorage/SyncedStorable.swift | 2 +- LeStorage/WebSocketManager.swift | 9 ++-- LeStorageTests/ApiCallTests.swift | 10 ++-- LeStorageTests/CollectionsTests.swift | 7 +-- LeStorageTests/IdentifiableTests.swift | 8 +-- LeStorageTests/StoredCollectionTests.swift | 4 +- 13 files changed, 134 insertions(+), 105 deletions(-) diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 8c882d9..f21fc96 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -39,6 +39,8 @@ enum ApiCallError: Error, LocalizedError { /// Failing Api calls are stored forever and will be executed again later actor ApiCallCollection: SomeCallCollection { + fileprivate var storeCenter: StoreCenter + /// The list of api calls fileprivate(set) var items: [ApiCall] = [] @@ -60,6 +62,10 @@ actor ApiCallCollection: SomeCallCollection { } } } + + init(storeCenter: StoreCenter) { + self.storeCenter = storeCenter + } /// Starts the JSON file decoding synchronously or asynchronously /// Reschedule Api calls if not empty @@ -185,7 +191,7 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _waitAndExecuteApiCalls() async { // Logger.log("\(T.resourceName()) > RESCHED") - guard !self._isExecutingCalls, StoreCenter.main.forceNoSynchronization == false else { return } + guard !self._isExecutingCalls, self.storeCenter.forceNoSynchronization == false else { return } guard self.items.isNotEmpty else { return } self._isExecutingCalls = true @@ -235,7 +241,7 @@ actor ApiCallCollection: SomeCallCollection { let results = try await self._executeApiCalls(batch) if T.copyServerResponse { let instances = results.compactMap { $0.data } - StoreCenter.main.updateLocalInstances(instances) + self.storeCenter.updateLocalInstances(instances) } } } catch { @@ -246,10 +252,10 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _executeGetCall(apiCall: ApiCall) async throws { if T.self == GetSyncData.self { - let _: Empty = try await StoreCenter.main.executeGet(apiCall: apiCall) + let _: Empty = try await self.storeCenter.executeGet(apiCall: apiCall) } else { - let results: [T] = try await StoreCenter.main.executeGet(apiCall: apiCall) - await StoreCenter.main.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive) + let results: [T] = try await self.storeCenter.executeGet(apiCall: apiCall) + await self.storeCenter.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive) } } @@ -333,7 +339,7 @@ actor ApiCallCollection: SomeCallCollection { /// Sends a GET request with an URLParameterConvertible [instance] func sendGetRequest(instance: URLParameterConvertible) async throws { - let parameters = instance.queryParameters() + let parameters = instance.queryParameters(storeCenter: self.storeCenter) try await self._sendGetRequest(parameters: parameters) } @@ -394,7 +400,7 @@ actor ApiCallCollection: SomeCallCollection { /// Executes an API call /// For POST requests, potentially copies additional data coming from the server during the insert fileprivate func _executeApiCalls(_ apiCalls: [ApiCall]) async throws -> [OperationResult] { - let results = try await StoreCenter.main.execute(apiCalls: apiCalls) + let results = try await self.storeCenter.execute(apiCalls: apiCalls) for result in results { switch result.status { case 200..<300: diff --git a/LeStorage/BaseCollection.swift b/LeStorage/BaseCollection.swift index 55c5c8c..21c0962 100644 --- a/LeStorage/BaseCollection.swift +++ b/LeStorage/BaseCollection.swift @@ -78,10 +78,12 @@ public class BaseCollection: SomeCollection, CollectionHolder { } - init() { - self.store = Store.main + init(store: Store) { + self.store = store } + var storeCenter: StoreCenter { return self.store.storeCenter } + /// Returns the name of the managed resource public var resourceName: String { return T.resourceName() @@ -400,7 +402,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { try self.store.write(content: jsonString, fileName: T.fileName()) } catch { Logger.error(error) - StoreCenter.main.log( + self.storeCenter.log( message: "write failed for \(T.resourceName()): \(error.localizedDescription)") } } @@ -440,7 +442,7 @@ public class StoredCollection: BaseCollection, RandomAccessColle /// Returns a dummy StoredCollection instance public static func placeholder() -> StoredCollection { - return StoredCollection() + return StoredCollection(store: Store(storeCenter: StoreCenter.main)) } // MARK: - RandomAccessCollection diff --git a/LeStorage/Codables/GetSyncData.swift b/LeStorage/Codables/GetSyncData.swift index 7d246b8..7c8d9e9 100644 --- a/LeStorage/Codables/GetSyncData.swift +++ b/LeStorage/Codables/GetSyncData.swift @@ -30,9 +30,9 @@ class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible { self.date = getSyncData.date } - func queryParameters() -> [String : String] { + func queryParameters(storeCenter: StoreCenter) -> [String : String] { return ["last_update" : self._formattedLastUpdate, - "device_id" : StoreCenter.main.deviceId()] + "device_id" : storeCenter.deviceId()] } fileprivate var _formattedLastUpdate: String { diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 0938ff5..86879a1 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -41,10 +41,13 @@ let userNamesCall: ServiceCall = ServiceCall( /// A class used to send HTTP request to the django server public class Services { + fileprivate let storeCenter: StoreCenter + /// The base API URL to send requests fileprivate(set) var baseURL: String - public init(url: String) { + public init(storeCenter: StoreCenter, url: String) { + self.storeCenter = storeCenter self.baseURL = url } @@ -91,10 +94,10 @@ public class Services { print("\(debugURL) ended, status code = \(statusCode)") switch statusCode { case 200..<300: // success - try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id) + try await self.storeCenter.deleteApiCallById(type: T.self, id: apiCall.id) if T.self == GetSyncData.self { - await StoreCenter.main.synchronizeContent(task.0) + await self.storeCenter.synchronizeContent(task.0) } default: // error @@ -107,8 +110,8 @@ public class Services { errorMessage = message } - try await StoreCenter.main.rescheduleApiCalls(type: T.self) - StoreCenter.main.logFailedAPICall( + try await self.storeCenter.rescheduleApiCalls(type: T.self) + self.storeCenter.logFailedAPICall( apiCall.id, request: request, collectionName: T.resourceName(), error: errorMessage.message) @@ -116,7 +119,7 @@ public class Services { } } else { let message: String = "Unexpected and unmanaged URL Response \(task.1)" - StoreCenter.main.log(message: message) + self.storeCenter.log(message: message) Logger.w(message) } @@ -160,7 +163,7 @@ public class Services { } } else { let message: String = "Unexpected and unmanaged URL Response \(task.1)" - StoreCenter.main.log(message: message) + self.storeCenter.log(message: message) Logger.w(message) } return try self._decode(data: task.0) @@ -258,7 +261,7 @@ public class Services { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.addAppVersion() if !(requiresToken == false) { - let token = try StoreCenter.main.token() + let token = try self.storeCenter.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") } return request @@ -313,9 +316,9 @@ public class Services { if let message = self.errorMessageFromResponse(data: task.0) { errorMessage = message } - try await StoreCenter.main.rescheduleApiCalls(type: T.self) + try await self.storeCenter.rescheduleApiCalls(type: T.self) - // StoreCenter.main.logFailedAPICall( + // self.storeCenter.logFailedAPICall( // apiCall.id, request: request, collectionName: T.resourceName(), // error: errorMessage.message) @@ -323,12 +326,12 @@ public class Services { } } else { let message: String = "Unexpected and unmanaged URL Response \(task.1)" - StoreCenter.main.log(message: message) + self.storeCenter.log(message: message) Logger.w(message) } if rescheduleApiCalls { - try? await StoreCenter.main.rescheduleApiCalls(type: T.self) + try? await self.storeCenter.rescheduleApiCalls(type: T.self) } return results @@ -353,8 +356,8 @@ public class Services { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.addAppVersion() - if self._isTokenRequired(type: T.self, method: apiCall.method), StoreCenter.main.isAuthenticated { - let token = try StoreCenter.main.token() + if self._isTokenRequired(type: T.self, method: apiCall.method), self.storeCenter.isAuthenticated { + let token = try self.storeCenter.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") } @@ -396,7 +399,7 @@ public class Services { var request = URLRequest(url: url) request.httpMethod = HTTPMethod.post.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") - let token = try StoreCenter.main.token() + let token = try self.storeCenter.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") request.addAppVersion() @@ -411,7 +414,7 @@ public class Services { } let payload = SyncPayload(operations: operations, - deviceId: StoreCenter.main.deviceId()) + deviceId: self.storeCenter.deviceId()) request.httpBody = try JSON.encoder.encode(payload) return request @@ -423,7 +426,7 @@ public class Services { func synchronizeLastUpdates(since: Date?) async throws { let request = try self._getSyncLogRequest(since: since) if let data = try await self._runRequest(request) { - await StoreCenter.main.synchronizeContent(data) + await self.storeCenter.synchronizeContent(data) } } @@ -447,7 +450,7 @@ public class Services { request.httpMethod = HTTPMethod.get.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") - let token = try StoreCenter.main.token() + let token = try self.storeCenter.token() request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") return request @@ -482,7 +485,7 @@ public class Services { } } else { let message: String = "Unexpected and unmanaged URL Response \(task.1)" - StoreCenter.main.log(message: message) + self.storeCenter.log(message: message) Logger.w(message) } return nil @@ -540,7 +543,7 @@ public class Services { request.addAppVersion() if self._isTokenRequired(type: T.self, method: apiCall.method) { do { - let token = try StoreCenter.main.token() + let token = try self.storeCenter.token() request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") } catch { Logger.log("missing token") @@ -585,14 +588,14 @@ public class Services { /// - password: the account's password public func requestToken(username: String, password: String) async throws -> String { var postRequest = try self._baseRequest(call: requestTokenCall) - let deviceId = StoreCenter.main.deviceId() + let deviceId = self.storeCenter.deviceId() let deviceModel = await UIDevice.current.deviceModel() let credentials = Credentials(username: username, password: password, deviceId: deviceId, deviceModel: deviceModel) postRequest.httpBody = try JSON.encoder.encode(credentials) let response: AuthResponse = try await self._runRequest(postRequest) - try StoreCenter.main.storeToken(username: username, token: response.token) + try self.storeCenter.storeToken(username: username, token: response.token) return response.token } @@ -606,7 +609,7 @@ public class Services { let postRequest = try self._baseRequest(call: getUserCall) let loggingDate = Date() // ideally we want the date of the latest retrieved object when loading collection objects let user: U = try await self._runRequest(postRequest) - StoreCenter.main.userDidLogIn(user: user, at: loggingDate) + self.storeCenter.userDidLogIn(user: user, at: loggingDate) return user } @@ -615,7 +618,7 @@ public class Services { /// - username: the account's username /// - password: the account's password public func logout() async throws { - let deviceId: String = StoreCenter.main.deviceId() + let deviceId: String = self.storeCenter.deviceId() let _: Empty = try await self._runRequest( serviceCall: logoutCall, payload: Logout(deviceId: deviceId)) } @@ -635,7 +638,7 @@ public class Services { func getUserDataAccess() async throws { let request = try self._baseRequest(call: getUserDataAccessCall) if let data = try await self._runRequest(request) { - await StoreCenter.main.userDataAccessRetrieved(data) + await self.storeCenter.userDataAccessRetrieved(data) } } @@ -648,7 +651,7 @@ public class Services { async throws { - guard let username = StoreCenter.main.userName else { + guard let username = self.storeCenter.userName else { throw ServiceError.missingUserName } @@ -663,7 +666,7 @@ public class Services { let response: Token = try await self._runRequest( serviceCall: changePasswordCall, payload: params) - try StoreCenter.main.storeToken(username: username, token: response.token) + try self.storeCenter.storeToken(username: username, token: response.token) } /// The method send a request to reset the user's password @@ -681,7 +684,7 @@ public class Services { /// - username: the account's username /// - password: the account's password public func deleteAccount() async throws { - guard let userId = StoreCenter.main.userId else { + guard let userId = self.storeCenter.userId else { throw StoreError.missingUserId } let path = "users/\(userId)/" diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 6415e08..24dbb2b 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -43,8 +43,10 @@ public enum StoreError: Error, LocalizedError { final public class Store { + fileprivate(set) var storeCenter: StoreCenter + /// The Store singleton - public static let main = Store() +// public static let main = Store() /// The dictionary of registered collections fileprivate var _collections: [String : any SomeCollection] = [:] @@ -55,16 +57,20 @@ final public class Store { /// The store identifier, used to name the store directory, and to perform filtering requests to the server public fileprivate(set) var identifier: String? = nil - public init() { + public init(storeCenter: StoreCenter) { + self.storeCenter = storeCenter self._createDirectory(directory: Store.storageDirectory) } - public required init(identifier: String) { + public required init(storeCenter: StoreCenter, identifier: String) { + self.storeCenter = storeCenter self.identifier = identifier let directory = "\(Store.storageDirectory)/\(identifier)" self._createDirectory(directory: directory) } + public static var main: Store { return StoreCenter.main.mainStore } + /// Creates the store directory /// - Parameters: /// - directory: the name of the directory @@ -105,7 +111,7 @@ final public class Store { let collection = SyncedCollection(store: self, indexed: indexed, inMemory: inMemory, limit: limit) self._collections[T.resourceName()] = collection - StoreCenter.main.loadApiCallCollection(type: T.self) + self.storeCenter.loadApiCallCollection(type: T.self) return collection } @@ -120,7 +126,7 @@ final public class Store { self._collections[T.resourceName()] = storedObject if synchronized { - StoreCenter.main.loadApiCallCollection(type: T.self) + self.storeCenter.loadApiCallCollection(type: T.self) } return storedObject @@ -298,9 +304,9 @@ final public class Store { /// Retrieves all the items on the server public func getItems() async throws -> [T] { if let identifier = self.identifier { - return try await StoreCenter.main.getItems(identifier: identifier) + return try await self.storeCenter.getItems(identifier: identifier) } else { - return try await StoreCenter.main.getItems() + return try await self.storeCenter.getItems() } } diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 4ca605c..9c31042 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -16,6 +16,8 @@ public class StoreCenter { /// A dictionary of Stores associated to their id fileprivate var _stores: [String: Store] = [:] + lazy var mainStore: Store = { Store(storeCenter: self) }() + /// A KeychainStore object used to store the user's token var keychainStore: KeychainStore? = nil @@ -36,7 +38,7 @@ public class StoreCenter { fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] /// A collection of DataLog objects, used for the synchronization - fileprivate var _dataLogs: StoredCollection + lazy fileprivate var _deleteLogs: StoredCollection = { self.mainStore.registerCollection() }() /// A synchronized collection of DataAccess fileprivate var _dataAccess: SyncedCollection? = nil @@ -53,11 +55,11 @@ public class StoreCenter { /// The URL manager fileprivate var _urlManager: URLManager? = nil + /// Memory only alternate device id for testing purpose + var alternateDeviceId: String? = nil + init() { -// self._syncGetRequests = ApiCallCollection() - self._dataLogs = Store.main.registerCollection() - self._setupNotifications() self.loadApiCallCollection(type: GetSyncData.self) @@ -72,10 +74,10 @@ public class StoreCenter { public func configureURLs(secureScheme: Bool, domain: String) { let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) self._urlManager = urlManager - self._services = Services(url: urlManager.api) + self._services = Services(storeCenter: self, url: urlManager.api) self.keychainStore = KeychainStore(serverId: urlManager.api) - self._dataAccess = Store.main.registerSynchronizedCollection() + self._dataAccess = self.mainStore.registerSynchronizedCollection() Logger.log("Sync URL: \(urlManager.api)") @@ -98,7 +100,7 @@ public class StoreCenter { return } let url = urlManager.websocket(userId: userId) - self._webSocketManager = WebSocketManager(urlString: url) + self._webSocketManager = WebSocketManager(storeCenter: self, urlString: url) Logger.log("websocket configured: \(url)") } @@ -173,7 +175,7 @@ public class StoreCenter { if let store = self._stores[identifier] { return store } else { - let store = Store(identifier: identifier) + let store = Store(storeCenter: self, identifier: identifier) self._registerStore(store: store) return store } @@ -220,7 +222,7 @@ public class StoreCenter { self._stores.removeAll() self._dataAccess?.reset() - self._dataLogs.reset() + self._deleteLogs.reset() self._settingsStorage.update { settings in settings.username = nil @@ -258,6 +260,10 @@ public class StoreCenter { /// 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 func deviceId() -> String { + if let alternateDeviceId { + return alternateDeviceId + } + let keychainStore = KeychainStore(serverId: "lestorage.main") do { return try keychainStore.getValue() @@ -278,7 +284,7 @@ public class StoreCenter { /// Instantiates and loads an ApiCallCollection with the provided type public func loadApiCallCollection(type: T.Type) { if self._apiCallCollections[T.resourceName()] == nil { - let apiCallCollection = ApiCallCollection() + let apiCallCollection = ApiCallCollection(storeCenter: self) self._apiCallCollections[T.resourceName()] = apiCallCollection Task { do { @@ -491,7 +497,7 @@ public class StoreCenter { /// Loads all the data from the server for the users public func initialSynchronization(clear: Bool) { - Store.main.loadCollectionsFromServer(clear: clear) + self.mainStore.loadCollectionsFromServer(clear: clear) // request data that has been shared with the user Task { @@ -607,7 +613,7 @@ public class StoreCenter { } } catch { - StoreCenter.main.log(message: error.localizedDescription) + self.log(message: error.localizedDescription) Logger.error(error) } @@ -640,7 +646,7 @@ public class StoreCenter { // Logger.log(">>> \(decodedObject.lastUpdate.timeIntervalSince1970) : \(decodedObject.id)") let storeId: String? = decodedObject.getStoreId() - StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) + self.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) } catch { Logger.w("Issue with json decoding: \(updateItem)") Logger.error(error) @@ -663,7 +669,7 @@ public class StoreCenter { let data = try JSONSerialization.data(withJSONObject: deleted, options: []) let deletedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) - StoreCenter.main.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) + self.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) } catch { Logger.error(error) } @@ -683,7 +689,7 @@ public class StoreCenter { do { let data = try JSONSerialization.data(withJSONObject: revoked, options: []) let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) - StoreCenter.main.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) + self.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) } catch { Logger.error(error) } @@ -707,7 +713,7 @@ public class StoreCenter { do { let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) - StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) + self.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) } catch { Logger.error(error) } @@ -736,18 +742,18 @@ public class StoreCenter { if let store = self._stores[storeId] { return store } else { - let store = Store(identifier: storeId) + let store = Store(storeCenter: self, identifier: storeId) self._registerStore(store: store) return store } } else { - return Store.main + return self.mainStore } } /// Returns whether a data has already been deleted by, to avoid inserting it again fileprivate func _hasAlreadyBeenDeleted(_ instance: T) -> Bool { - return self._dataLogs.contains(where: { + return self._deleteLogs.contains(where: { $0.dataId == instance.stringId && $0.operation == .delete }) } @@ -783,7 +789,7 @@ public class StoreCenter { do { let type = try StoreCenter.classFromName(model) if self._instanceShared(id: id, type: type) { - let count = Store.main.referenceCount(type: type, id: id) + let count = self.mainStore.referenceCount(type: type, id: id) if count == 0 { try self._store(id: storeId).deleteNoSync(type: type, id: id) } @@ -797,14 +803,14 @@ public class StoreCenter { /// Returns whether an instance has been shared with the user fileprivate func _instanceShared(id: String, type: T.Type) -> Bool { let realId: T.ID = T.buildRealId(id: id) - let instance: T? = Store.main.findById(realId) + let instance: T? = self.mainStore.findById(realId) return instance?.shared == true } /// Deletes a data log by data id fileprivate func _cleanupDataLog(dataId: String) { - let logs = self._dataLogs.filter { $0.dataId == dataId } - self._dataLogs.delete(contentOfs: logs) + let logs = self._deleteLogs.filter { $0.dataId == dataId } + self._deleteLogs.delete(contentOfs: logs) } /// Creates a delete log for an instance @@ -816,7 +822,7 @@ public class StoreCenter { fileprivate func _addDataLog(_ instance: T, method: HTTPMethod) { let dataLog = DataLog( dataId: instance.stringId, modelName: String(describing: T.self), operation: method) - self._dataLogs.addOrUpdate(instance: dataLog) + self._deleteLogs.addOrUpdate(instance: dataLog) } // MARK: - Miscellanous @@ -863,7 +869,7 @@ public class StoreCenter { /// This method triggers the framework to save and send failed api calls public func logsFailedAPICalls() { - self._failedAPICallsCollection = Store.main.registerSynchronizedCollection(limit: 50) + self._failedAPICallsCollection = self.mainStore.registerSynchronizedCollection(limit: 50) } /// If configured for, logs and send to the server a failed API call @@ -963,7 +969,7 @@ public class StoreCenter { /// Returns the collection hosting an instance func collectionOfInstance(_ instance: T) -> BaseCollection? { do { - let collection: BaseCollection = try Store.main.collection() + let collection: BaseCollection = try self.mainStore.collection() if collection.findById(instance.id) != nil { return collection } else { @@ -1028,7 +1034,7 @@ public class StoreCenter { if let logs = self._logs { return logs } else { - let logsCollection: SyncedCollection = Store.main.registerSynchronizedCollection(limit: 50) + let logsCollection: SyncedCollection = self.mainStore.registerSynchronizedCollection(limit: 50) self._logs = logsCollection return logsCollection } diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index 8dd8c18..72f4c68 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -16,7 +16,7 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Returns a dummy SyncedCollection instance public static func placeholder() -> SyncedCollection { - return SyncedCollection() + return SyncedCollection(store: Store(storeCenter: StoreCenter.main)) } /// Migrates if necessary and asynchronously decodes the json file @@ -50,7 +50,7 @@ public class SyncedCollection: BaseCollection, SomeSynced throw StoreError.cannotSyncCollection(name: self.resourceName) } do { - try await StoreCenter.main.sendGetRequest(T.self, storeId: self.storeId, clear: clear) + try await self.storeCenter.sendGetRequest(T.self, storeId: self.storeId, clear: clear) } catch { Logger.error(error) } @@ -211,7 +211,7 @@ public class SyncedCollection: BaseCollection, SomeSynced if self.deleteItem(instance, shouldBeSynchronized: true) { deleted.append(instance) } - StoreCenter.main.createDeleteLog(instance) + self.storeCenter.createDeleteLog(instance) } let batch = OperationBatch() @@ -254,7 +254,7 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Deletes an instance without writing, logs the operation and sends an API call fileprivate func _deleteNoWrite(instance: T) { self.deleteItem(instance, shouldBeSynchronized: true) - StoreCenter.main.createDeleteLog(instance) + self.storeCenter.createDeleteLog(instance) // await self._sendDeletion(instance) } @@ -324,7 +324,7 @@ public class SyncedCollection: BaseCollection, SomeSynced fileprivate func _sendOperationBatch(_ batch: OperationBatch) async { do { - try await StoreCenter.main.sendOperationBatch(batch) + try await self.storeCenter.sendOperationBatch(batch) } catch { Logger.error(error) } @@ -332,7 +332,7 @@ public class SyncedCollection: BaseCollection, SomeSynced fileprivate func _executeBatchOnce(_ batch: OperationBatch) async { do { - try await StoreCenter.main.singleBatchExecution(batch) + try await self.storeCenter.singleBatchExecution(batch) } catch { Logger.error(error) } @@ -341,13 +341,13 @@ public class SyncedCollection: BaseCollection, SomeSynced // MARK: Single calls public func addsIfPostSucceeds(_ instance: T) async throws { - if let result = try await StoreCenter.main.service().post(instance) { + if let result = try await self.storeCenter.service().post(instance) { self.addOrUpdateNoSync(result) } } public func updateIfPutSucceeds(_ instance: T) async throws { - if let result = try await StoreCenter.main.service().put(instance) { + if let result = try await self.storeCenter.service().put(instance) { self.addOrUpdateNoSync(result) } } @@ -422,11 +422,13 @@ public class SyncedCollection: BaseCollection, SomeSynced // MARK: - Others /// Sends a POST request for the instance, and changes the collection to perform a write - public func writeChangeAndInsertOnServer(instance: T) async { - defer { - self.setChanged() + public func writeChangeAndInsertOnServer(instance: T) { + Task { + await self._sendInsertion(instance) + await MainActor.run { + self.setChanged() + } } - await self._sendInsertion(instance) } } diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index f997542..484b7a9 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -23,7 +23,7 @@ public protocol SyncedStorable: Storable { } protocol URLParameterConvertible { - func queryParameters() -> [String : String] + func queryParameters(storeCenter: StoreCenter) -> [String : String] } public protocol SideStorable { diff --git a/LeStorage/WebSocketManager.swift b/LeStorage/WebSocketManager.swift index c3b7c0b..85b3ade 100644 --- a/LeStorage/WebSocketManager.swift +++ b/LeStorage/WebSocketManager.swift @@ -10,6 +10,8 @@ import SwiftUI import Combine class WebSocketManager: ObservableObject { + + fileprivate(set) var storeCenter: StoreCenter fileprivate var _webSocketTask: URLSessionWebSocketTask? fileprivate var _timer: Timer? @@ -19,7 +21,8 @@ class WebSocketManager: ObservableObject { fileprivate var _failure = false fileprivate var _pingOk = false - init(urlString: String) { + init(storeCenter: StoreCenter, urlString: String) { + self.storeCenter = storeCenter self._url = urlString _setupWebSocket() } @@ -63,13 +66,13 @@ class WebSocketManager: ObservableObject { switch message { case .string(let deviceId): // print("device id = \(StoreCenter.main.deviceId()), origin id: \(deviceId)") - guard StoreCenter.main.deviceId() != deviceId else { + guard self.storeCenter.deviceId() != deviceId else { break } Task { do { - try await StoreCenter.main.synchronizeLastUpdates() + try await self.storeCenter.synchronizeLastUpdates() } catch { Logger.error(error) } diff --git a/LeStorageTests/ApiCallTests.swift b/LeStorageTests/ApiCallTests.swift index e68b142..7ec6a30 100644 --- a/LeStorageTests/ApiCallTests.swift +++ b/LeStorageTests/ApiCallTests.swift @@ -34,7 +34,7 @@ class Thing: SyncedModelObject, SyncedStorable, URLParameterConvertible { static func relationships() -> [LeStorage.Relationship] { return [] } - func queryParameters() -> [String : String] { + func queryParameters(storeCenter: StoreCenter) -> [String : String] { return ["yeah?" : "god!"] } @@ -43,7 +43,7 @@ class Thing: SyncedModelObject, SyncedStorable, URLParameterConvertible { struct ApiCallTests { @Test func testApiCallProvisioning1() async throws { - let collection = ApiCallCollection() + let collection = ApiCallCollection(storeCenter: StoreCenter.main) let thing = Thing(name: "yeah") @@ -69,7 +69,7 @@ struct ApiCallTests { } @Test func testApiCallProvisioning2() async throws { - let collection = ApiCallCollection() + let collection = ApiCallCollection(storeCenter: StoreCenter.main) let thing = Thing(name: "yeah") @@ -94,7 +94,7 @@ struct ApiCallTests { } @Test func testApiCallProvisioning3() async throws { - let collection = ApiCallCollection() + let collection = ApiCallCollection(storeCenter: StoreCenter.main) let thing = Thing(name: "yeah") @@ -107,7 +107,7 @@ struct ApiCallTests { } @Test func testGetProvisioning() async throws { - let collection = ApiCallCollection() + let collection = ApiCallCollection(storeCenter: StoreCenter.main) try await collection.sendGetRequest(storeId: "1") await #expect(collection.items.count == 1) diff --git a/LeStorageTests/CollectionsTests.swift b/LeStorageTests/CollectionsTests.swift index 77ca937..8d7c022 100644 --- a/LeStorageTests/CollectionsTests.swift +++ b/LeStorageTests/CollectionsTests.swift @@ -6,7 +6,7 @@ // import Testing -import LeStorage +@testable import LeStorage class Car: ModelObject, Storable { @@ -47,8 +47,9 @@ struct CollectionsTests { var boats: SyncedCollection init() { - cars = Store.main.registerCollection(inMemory: true) - boats = Store.main.registerSynchronizedCollection(inMemory: true) + let storeCenter = StoreCenter.main + cars = storeCenter.mainStore.registerCollection(inMemory: true) + boats = storeCenter.mainStore.registerSynchronizedCollection(inMemory: true) } func ensureCollectionLoaded(_ collection: any SomeCollection) async throws { diff --git a/LeStorageTests/IdentifiableTests.swift b/LeStorageTests/IdentifiableTests.swift index 493b156..0748c28 100644 --- a/LeStorageTests/IdentifiableTests.swift +++ b/LeStorageTests/IdentifiableTests.swift @@ -6,7 +6,7 @@ // import Testing -import LeStorage +@testable import LeStorage class IntObject: ModelObject, Storable { @@ -32,7 +32,6 @@ class IntObject: ModelObject, Storable { class StringObject: ModelObject, Storable { static func resourceName() -> String { "string" } static func tokenExemptedMethods() -> [LeStorage.HTTPMethod] { [] } - static var relationshipNames: [String] = [] var id: String var name: String @@ -56,8 +55,9 @@ struct IdentifiableTests { let stringObjects: StoredCollection init() { - intObjects = Store.main.registerCollection() - stringObjects = Store.main.registerCollection() + let storeCenter = StoreCenter.main + intObjects = storeCenter.mainStore.registerCollection() + stringObjects = storeCenter.mainStore.registerCollection() } func ensureCollectionLoaded(_ collection: any SomeCollection) async throws { diff --git a/LeStorageTests/StoredCollectionTests.swift b/LeStorageTests/StoredCollectionTests.swift index a1772d8..0ef37ba 100644 --- a/LeStorageTests/StoredCollectionTests.swift +++ b/LeStorageTests/StoredCollectionTests.swift @@ -6,7 +6,7 @@ // import Testing -import LeStorage +@testable import LeStorage struct Error: Swift.Error, CustomStringConvertible { let description: String @@ -21,7 +21,7 @@ struct StoredCollectionTests { var collection: StoredCollection init() { - collection = Store.main.registerCollection() + collection = StoreCenter.main.mainStore.registerCollection() } func ensureCollectionLoaded() async throws {