Refactoring to pass a reference of StoreCenter in the various classes

sync_v2
Laurent 7 months ago
parent b32b0f2a74
commit c8f204462a
  1. 20
      LeStorage/ApiCallCollection.swift
  2. 10
      LeStorage/BaseCollection.swift
  3. 4
      LeStorage/Codables/GetSyncData.swift
  4. 59
      LeStorage/Services.swift
  5. 20
      LeStorage/Store.swift
  6. 60
      LeStorage/StoreCenter.swift
  7. 26
      LeStorage/SyncedCollection.swift
  8. 2
      LeStorage/SyncedStorable.swift
  9. 9
      LeStorage/WebSocketManager.swift
  10. 10
      LeStorageTests/ApiCallTests.swift
  11. 7
      LeStorageTests/CollectionsTests.swift
  12. 8
      LeStorageTests/IdentifiableTests.swift
  13. 4
      LeStorageTests/StoredCollectionTests.swift

@ -39,6 +39,8 @@ enum ApiCallError: Error, LocalizedError {
/// Failing Api calls are stored forever and will be executed again later /// Failing Api calls are stored forever and will be executed again later
actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate var storeCenter: StoreCenter
/// The list of api calls /// The list of api calls
fileprivate(set) var items: [ApiCall<T>] = [] fileprivate(set) var items: [ApiCall<T>] = []
@ -61,6 +63,10 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
} }
} }
init(storeCenter: StoreCenter) {
self.storeCenter = storeCenter
}
/// Starts the JSON file decoding synchronously or asynchronously /// Starts the JSON file decoding synchronously or asynchronously
/// Reschedule Api calls if not empty /// Reschedule Api calls if not empty
func loadFromFile() throws { func loadFromFile() throws {
@ -185,7 +191,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate func _waitAndExecuteApiCalls() async { fileprivate func _waitAndExecuteApiCalls() async {
// Logger.log("\(T.resourceName()) > RESCHED") // 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 } guard self.items.isNotEmpty else { return }
self._isExecutingCalls = true self._isExecutingCalls = true
@ -235,7 +241,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
let results = try await self._executeApiCalls(batch) let results = try await self._executeApiCalls(batch)
if T.copyServerResponse { if T.copyServerResponse {
let instances = results.compactMap { $0.data } let instances = results.compactMap { $0.data }
StoreCenter.main.updateLocalInstances(instances) self.storeCenter.updateLocalInstances(instances)
} }
} }
} catch { } catch {
@ -246,10 +252,10 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate func _executeGetCall(apiCall: ApiCall<T>) async throws { fileprivate func _executeGetCall(apiCall: ApiCall<T>) async throws {
if T.self == GetSyncData.self { if T.self == GetSyncData.self {
let _: Empty = try await StoreCenter.main.executeGet(apiCall: apiCall) let _: Empty = try await self.storeCenter.executeGet(apiCall: apiCall)
} else { } else {
let results: [T] = try await StoreCenter.main.executeGet(apiCall: apiCall) let results: [T] = try await self.storeCenter.executeGet(apiCall: apiCall)
await StoreCenter.main.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive) await self.storeCenter.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive)
} }
} }
@ -333,7 +339,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Sends a GET request with an URLParameterConvertible [instance] /// Sends a GET request with an URLParameterConvertible [instance]
func sendGetRequest(instance: URLParameterConvertible) async throws { func sendGetRequest(instance: URLParameterConvertible) async throws {
let parameters = instance.queryParameters() let parameters = instance.queryParameters(storeCenter: self.storeCenter)
try await self._sendGetRequest(parameters: parameters) try await self._sendGetRequest(parameters: parameters)
} }
@ -394,7 +400,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Executes an API call /// Executes an API call
/// For POST requests, potentially copies additional data coming from the server during the insert /// For POST requests, potentially copies additional data coming from the server during the insert
fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws -> [OperationResult<T>] { fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws -> [OperationResult<T>] {
let results = try await StoreCenter.main.execute(apiCalls: apiCalls) let results = try await self.storeCenter.execute(apiCalls: apiCalls)
for result in results { for result in results {
switch result.status { switch result.status {
case 200..<300: case 200..<300:

@ -78,10 +78,12 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
} }
init() { init(store: Store) {
self.store = Store.main self.store = store
} }
var storeCenter: StoreCenter { return self.store.storeCenter }
/// Returns the name of the managed resource /// Returns the name of the managed resource
public var resourceName: String { public var resourceName: String {
return T.resourceName() return T.resourceName()
@ -400,7 +402,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
try self.store.write(content: jsonString, fileName: T.fileName()) try self.store.write(content: jsonString, fileName: T.fileName())
} catch { } catch {
Logger.error(error) Logger.error(error)
StoreCenter.main.log( self.storeCenter.log(
message: "write failed for \(T.resourceName()): \(error.localizedDescription)") message: "write failed for \(T.resourceName()): \(error.localizedDescription)")
} }
} }
@ -440,7 +442,7 @@ public class StoredCollection<T: Storable>: BaseCollection<T>, RandomAccessColle
/// Returns a dummy StoredCollection instance /// Returns a dummy StoredCollection instance
public static func placeholder() -> StoredCollection<T> { public static func placeholder() -> StoredCollection<T> {
return StoredCollection<T>() return StoredCollection<T>(store: Store(storeCenter: StoreCenter.main))
} }
// MARK: - RandomAccessCollection // MARK: - RandomAccessCollection

@ -30,9 +30,9 @@ class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible {
self.date = getSyncData.date self.date = getSyncData.date
} }
func queryParameters() -> [String : String] { func queryParameters(storeCenter: StoreCenter) -> [String : String] {
return ["last_update" : self._formattedLastUpdate, return ["last_update" : self._formattedLastUpdate,
"device_id" : StoreCenter.main.deviceId()] "device_id" : storeCenter.deviceId()]
} }
fileprivate var _formattedLastUpdate: String { fileprivate var _formattedLastUpdate: String {

@ -41,10 +41,13 @@ let userNamesCall: ServiceCall = ServiceCall(
/// A class used to send HTTP request to the django server /// A class used to send HTTP request to the django server
public class Services { public class Services {
fileprivate let storeCenter: StoreCenter
/// The base API URL to send requests /// The base API URL to send requests
fileprivate(set) var baseURL: String fileprivate(set) var baseURL: String
public init(url: String) { public init(storeCenter: StoreCenter, url: String) {
self.storeCenter = storeCenter
self.baseURL = url self.baseURL = url
} }
@ -91,10 +94,10 @@ public class Services {
print("\(debugURL) ended, status code = \(statusCode)") print("\(debugURL) ended, status code = \(statusCode)")
switch statusCode { switch statusCode {
case 200..<300: // success 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 { if T.self == GetSyncData.self {
await StoreCenter.main.synchronizeContent(task.0) await self.storeCenter.synchronizeContent(task.0)
} }
default: // error default: // error
@ -107,8 +110,8 @@ public class Services {
errorMessage = message 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(), apiCall.id, request: request, collectionName: T.resourceName(),
error: errorMessage.message) error: errorMessage.message)
@ -116,7 +119,7 @@ public class Services {
} }
} else { } else {
let message: String = "Unexpected and unmanaged URL Response \(task.1)" let message: String = "Unexpected and unmanaged URL Response \(task.1)"
StoreCenter.main.log(message: message) self.storeCenter.log(message: message)
Logger.w(message) Logger.w(message)
} }
@ -160,7 +163,7 @@ public class Services {
} }
} else { } else {
let message: String = "Unexpected and unmanaged URL Response \(task.1)" let message: String = "Unexpected and unmanaged URL Response \(task.1)"
StoreCenter.main.log(message: message) self.storeCenter.log(message: message)
Logger.w(message) Logger.w(message)
} }
return try self._decode(data: task.0) return try self._decode(data: task.0)
@ -258,7 +261,7 @@ public class Services {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.addAppVersion() request.addAppVersion()
if !(requiresToken == false) { if !(requiresToken == false) {
let token = try StoreCenter.main.token() let token = try self.storeCenter.token()
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
} }
return request return request
@ -313,9 +316,9 @@ public class Services {
if let message = self.errorMessageFromResponse(data: task.0) { if let message = self.errorMessageFromResponse(data: task.0) {
errorMessage = message 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(), // apiCall.id, request: request, collectionName: T.resourceName(),
// error: errorMessage.message) // error: errorMessage.message)
@ -323,12 +326,12 @@ public class Services {
} }
} else { } else {
let message: String = "Unexpected and unmanaged URL Response \(task.1)" let message: String = "Unexpected and unmanaged URL Response \(task.1)"
StoreCenter.main.log(message: message) self.storeCenter.log(message: message)
Logger.w(message) Logger.w(message)
} }
if rescheduleApiCalls { if rescheduleApiCalls {
try? await StoreCenter.main.rescheduleApiCalls(type: T.self) try? await self.storeCenter.rescheduleApiCalls(type: T.self)
} }
return results return results
@ -353,8 +356,8 @@ public class Services {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.addAppVersion() request.addAppVersion()
if self._isTokenRequired(type: T.self, method: apiCall.method), StoreCenter.main.isAuthenticated { if self._isTokenRequired(type: T.self, method: apiCall.method), self.storeCenter.isAuthenticated {
let token = try StoreCenter.main.token() let token = try self.storeCenter.token()
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
} }
@ -396,7 +399,7 @@ public class Services {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue request.httpMethod = HTTPMethod.post.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
request.addAppVersion() request.addAppVersion()
@ -411,7 +414,7 @@ public class Services {
} }
let payload = SyncPayload(operations: operations, let payload = SyncPayload(operations: operations,
deviceId: StoreCenter.main.deviceId()) deviceId: self.storeCenter.deviceId())
request.httpBody = try JSON.encoder.encode(payload) request.httpBody = try JSON.encoder.encode(payload)
return request return request
@ -423,7 +426,7 @@ public class Services {
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 StoreCenter.main.synchronizeContent(data) await self.storeCenter.synchronizeContent(data)
} }
} }
@ -447,7 +450,7 @@ public class Services {
request.httpMethod = HTTPMethod.get.rawValue request.httpMethod = HTTPMethod.get.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
return request return request
@ -482,7 +485,7 @@ public class Services {
} }
} else { } else {
let message: String = "Unexpected and unmanaged URL Response \(task.1)" let message: String = "Unexpected and unmanaged URL Response \(task.1)"
StoreCenter.main.log(message: message) self.storeCenter.log(message: message)
Logger.w(message) Logger.w(message)
} }
return nil return nil
@ -540,7 +543,7 @@ public class Services {
request.addAppVersion() request.addAppVersion()
if self._isTokenRequired(type: T.self, method: apiCall.method) { if self._isTokenRequired(type: T.self, method: apiCall.method) {
do { do {
let token = try StoreCenter.main.token() let token = try self.storeCenter.token()
request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
} catch { } catch {
Logger.log("missing token") Logger.log("missing token")
@ -585,14 +588,14 @@ public class Services {
/// - password: the account's password /// - password: the account's password
public func requestToken(username: String, password: String) async throws -> String { public func requestToken(username: String, password: String) async throws -> String {
var postRequest = try self._baseRequest(call: requestTokenCall) var postRequest = try self._baseRequest(call: requestTokenCall)
let deviceId = StoreCenter.main.deviceId() let deviceId = self.storeCenter.deviceId()
let deviceModel = await UIDevice.current.deviceModel() let deviceModel = await UIDevice.current.deviceModel()
let credentials = Credentials(username: username, password: password, deviceId: deviceId, deviceModel: deviceModel) let credentials = Credentials(username: username, password: password, deviceId: deviceId, deviceModel: deviceModel)
postRequest.httpBody = try JSON.encoder.encode(credentials) postRequest.httpBody = try JSON.encoder.encode(credentials)
let response: AuthResponse = try await self._runRequest(postRequest) 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 return response.token
} }
@ -606,7 +609,7 @@ public class Services {
let postRequest = try self._baseRequest(call: getUserCall) 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 loggingDate = Date() // ideally we want the date of the latest retrieved object when loading collection objects
let user: U = try await self._runRequest(postRequest) let user: U = try await self._runRequest(postRequest)
StoreCenter.main.userDidLogIn(user: user, at: loggingDate) self.storeCenter.userDidLogIn(user: user, at: loggingDate)
return user return user
} }
@ -615,7 +618,7 @@ public class Services {
/// - username: the account's username /// - username: the account's username
/// - password: the account's password /// - password: the account's password
public func logout() async throws { public func logout() async throws {
let deviceId: String = StoreCenter.main.deviceId() let deviceId: String = self.storeCenter.deviceId()
let _: Empty = try await self._runRequest( let _: Empty = try await self._runRequest(
serviceCall: logoutCall, payload: Logout(deviceId: deviceId)) serviceCall: logoutCall, payload: Logout(deviceId: deviceId))
} }
@ -635,7 +638,7 @@ public class Services {
func getUserDataAccess() async throws { func getUserDataAccess() async throws {
let request = try self._baseRequest(call: getUserDataAccessCall) let request = try self._baseRequest(call: getUserDataAccessCall)
if let data = try await self._runRequest(request) { 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 async throws
{ {
guard let username = StoreCenter.main.userName else { guard let username = self.storeCenter.userName else {
throw ServiceError.missingUserName throw ServiceError.missingUserName
} }
@ -663,7 +666,7 @@ public class Services {
let response: Token = try await self._runRequest( let response: Token = try await self._runRequest(
serviceCall: changePasswordCall, payload: params) 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 /// The method send a request to reset the user's password
@ -681,7 +684,7 @@ public class Services {
/// - username: the account's username /// - username: the account's username
/// - password: the account's password /// - password: the account's password
public func deleteAccount() async throws { public func deleteAccount() async throws {
guard let userId = StoreCenter.main.userId else { guard let userId = self.storeCenter.userId else {
throw StoreError.missingUserId throw StoreError.missingUserId
} }
let path = "users/\(userId)/" let path = "users/\(userId)/"

@ -43,8 +43,10 @@ public enum StoreError: Error, LocalizedError {
final public class Store { final public class Store {
fileprivate(set) var storeCenter: StoreCenter
/// The Store singleton /// The Store singleton
public static let main = Store() // public static let main = Store()
/// The dictionary of registered collections /// The dictionary of registered collections
fileprivate var _collections: [String : any SomeCollection] = [:] 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 /// 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 fileprivate(set) var identifier: String? = nil
public init() { public init(storeCenter: StoreCenter) {
self.storeCenter = storeCenter
self._createDirectory(directory: Store.storageDirectory) self._createDirectory(directory: Store.storageDirectory)
} }
public required init(identifier: String) { public required init(storeCenter: StoreCenter, identifier: String) {
self.storeCenter = storeCenter
self.identifier = identifier self.identifier = identifier
let directory = "\(Store.storageDirectory)/\(identifier)" let directory = "\(Store.storageDirectory)/\(identifier)"
self._createDirectory(directory: directory) self._createDirectory(directory: directory)
} }
public static var main: Store { return StoreCenter.main.mainStore }
/// Creates the store directory /// Creates the store directory
/// - Parameters: /// - Parameters:
/// - directory: the name of the directory /// - directory: the name of the directory
@ -105,7 +111,7 @@ final public class Store {
let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit) let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit)
self._collections[T.resourceName()] = collection self._collections[T.resourceName()] = collection
StoreCenter.main.loadApiCallCollection(type: T.self) self.storeCenter.loadApiCallCollection(type: T.self)
return collection return collection
} }
@ -120,7 +126,7 @@ final public class Store {
self._collections[T.resourceName()] = storedObject self._collections[T.resourceName()] = storedObject
if synchronized { if synchronized {
StoreCenter.main.loadApiCallCollection(type: T.self) self.storeCenter.loadApiCallCollection(type: T.self)
} }
return storedObject return storedObject
@ -298,9 +304,9 @@ final public class Store {
/// Retrieves all the items on the server /// Retrieves all the items on the server
public func getItems<T: SyncedStorable>() async throws -> [T] { public func getItems<T: SyncedStorable>() async throws -> [T] {
if let identifier = self.identifier { if let identifier = self.identifier {
return try await StoreCenter.main.getItems(identifier: identifier) return try await self.storeCenter.getItems(identifier: identifier)
} else { } else {
return try await StoreCenter.main.getItems() return try await self.storeCenter.getItems()
} }
} }

@ -16,6 +16,8 @@ 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] = [:]
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
var keychainStore: KeychainStore? = nil var keychainStore: KeychainStore? = nil
@ -36,7 +38,7 @@ public class StoreCenter {
fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:]
/// A collection of DataLog objects, used for the synchronization /// A collection of DataLog objects, used for the synchronization
fileprivate var _dataLogs: StoredCollection<DataLog> lazy fileprivate var _deleteLogs: StoredCollection<DataLog> = { self.mainStore.registerCollection() }()
/// A synchronized collection of DataAccess /// A synchronized collection of DataAccess
fileprivate var _dataAccess: SyncedCollection<DataAccess>? = nil fileprivate var _dataAccess: SyncedCollection<DataAccess>? = nil
@ -53,10 +55,10 @@ public class StoreCenter {
/// The URL manager /// The URL manager
fileprivate var _urlManager: URLManager? = nil fileprivate var _urlManager: URLManager? = nil
init() { /// Memory only alternate device id for testing purpose
var alternateDeviceId: String? = nil
// self._syncGetRequests = ApiCallCollection() init() {
self._dataLogs = Store.main.registerCollection()
self._setupNotifications() self._setupNotifications()
@ -72,10 +74,10 @@ public class StoreCenter {
public func configureURLs(secureScheme: Bool, domain: String) { public func configureURLs(secureScheme: Bool, domain: String) {
let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain)
self._urlManager = urlManager self._urlManager = urlManager
self._services = Services(url: urlManager.api) self._services = Services(storeCenter: self, url: urlManager.api)
self.keychainStore = KeychainStore(serverId: urlManager.api) self.keychainStore = KeychainStore(serverId: urlManager.api)
self._dataAccess = Store.main.registerSynchronizedCollection() self._dataAccess = self.mainStore.registerSynchronizedCollection()
Logger.log("Sync URL: \(urlManager.api)") Logger.log("Sync URL: \(urlManager.api)")
@ -98,7 +100,7 @@ public class StoreCenter {
return return
} }
let url = urlManager.websocket(userId: userId) let url = urlManager.websocket(userId: userId)
self._webSocketManager = WebSocketManager(urlString: url) self._webSocketManager = WebSocketManager(storeCenter: self, urlString: url)
Logger.log("websocket configured: \(url)") Logger.log("websocket configured: \(url)")
} }
@ -173,7 +175,7 @@ public class StoreCenter {
if let store = self._stores[identifier] { if let store = self._stores[identifier] {
return store return store
} else { } else {
let store = Store(identifier: identifier) let store = Store(storeCenter: self, identifier: identifier)
self._registerStore(store: store) self._registerStore(store: store)
return store return store
} }
@ -220,7 +222,7 @@ public class StoreCenter {
self._stores.removeAll() self._stores.removeAll()
self._dataAccess?.reset() self._dataAccess?.reset()
self._dataLogs.reset() self._deleteLogs.reset()
self._settingsStorage.update { settings in self._settingsStorage.update { settings in
settings.username = nil 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 /// 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 /// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again
func deviceId() -> String { func deviceId() -> String {
if let alternateDeviceId {
return alternateDeviceId
}
let keychainStore = KeychainStore(serverId: "lestorage.main") let keychainStore = KeychainStore(serverId: "lestorage.main")
do { do {
return try keychainStore.getValue() return try keychainStore.getValue()
@ -278,7 +284,7 @@ public class StoreCenter {
/// Instantiates and loads an ApiCallCollection with the provided type /// Instantiates and loads an ApiCallCollection with the provided type
public func loadApiCallCollection<T: SyncedStorable>(type: T.Type) { public func loadApiCallCollection<T: SyncedStorable>(type: T.Type) {
if self._apiCallCollections[T.resourceName()] == nil { if self._apiCallCollections[T.resourceName()] == nil {
let apiCallCollection = ApiCallCollection<T>() let apiCallCollection = ApiCallCollection<T>(storeCenter: self)
self._apiCallCollections[T.resourceName()] = apiCallCollection self._apiCallCollections[T.resourceName()] = apiCallCollection
Task { Task {
do { do {
@ -491,7 +497,7 @@ public class StoreCenter {
/// Loads all the data from the server for the users /// Loads all the data from the server for the users
public func initialSynchronization(clear: Bool) { public func initialSynchronization(clear: Bool) {
Store.main.loadCollectionsFromServer(clear: clear) self.mainStore.loadCollectionsFromServer(clear: clear)
// request data that has been shared with the user // request data that has been shared with the user
Task { Task {
@ -607,7 +613,7 @@ public class StoreCenter {
} }
} catch { } catch {
StoreCenter.main.log(message: error.localizedDescription) self.log(message: error.localizedDescription)
Logger.error(error) Logger.error(error)
} }
@ -640,7 +646,7 @@ public class StoreCenter {
// Logger.log(">>> \(decodedObject.lastUpdate.timeIntervalSince1970) : \(decodedObject.id)") // Logger.log(">>> \(decodedObject.lastUpdate.timeIntervalSince1970) : \(decodedObject.id)")
let storeId: String? = decodedObject.getStoreId() let storeId: String? = decodedObject.getStoreId()
StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) self.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared)
} catch { } catch {
Logger.w("Issue with json decoding: \(updateItem)") Logger.w("Issue with json decoding: \(updateItem)")
Logger.error(error) Logger.error(error)
@ -663,7 +669,7 @@ public class StoreCenter {
let data = try JSONSerialization.data(withJSONObject: deleted, options: []) let data = try JSONSerialization.data(withJSONObject: deleted, options: [])
let deletedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) 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 { } catch {
Logger.error(error) Logger.error(error)
} }
@ -683,7 +689,7 @@ public class StoreCenter {
do { do {
let data = try JSONSerialization.data(withJSONObject: revoked, options: []) let data = try JSONSerialization.data(withJSONObject: revoked, options: [])
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) 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 { } catch {
Logger.error(error) Logger.error(error)
} }
@ -707,7 +713,7 @@ public class StoreCenter {
do { do {
let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) let data = try JSONSerialization.data(withJSONObject: parentItem, options: [])
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) 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 { } catch {
Logger.error(error) Logger.error(error)
} }
@ -736,18 +742,18 @@ public class StoreCenter {
if let store = self._stores[storeId] { if let store = self._stores[storeId] {
return store return store
} else { } else {
let store = Store(identifier: storeId) let store = Store(storeCenter: self, identifier: storeId)
self._registerStore(store: store) self._registerStore(store: store)
return store return store
} }
} else { } else {
return Store.main return self.mainStore
} }
} }
/// Returns whether a data has already been deleted by, to avoid inserting it again /// Returns whether a data has already been deleted by, to avoid inserting it again
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool { fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool {
return self._dataLogs.contains(where: { return self._deleteLogs.contains(where: {
$0.dataId == instance.stringId && $0.operation == .delete $0.dataId == instance.stringId && $0.operation == .delete
}) })
} }
@ -783,7 +789,7 @@ public class StoreCenter {
do { do {
let type = try StoreCenter.classFromName(model) let type = try StoreCenter.classFromName(model)
if self._instanceShared(id: id, type: type) { 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 { if count == 0 {
try self._store(id: storeId).deleteNoSync(type: type, id: id) 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 /// Returns whether an instance has been shared with the user
fileprivate func _instanceShared<T: SyncedStorable>(id: String, type: T.Type) -> Bool { fileprivate func _instanceShared<T: SyncedStorable>(id: String, type: T.Type) -> Bool {
let realId: T.ID = T.buildRealId(id: id) 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 return instance?.shared == true
} }
/// Deletes a data log by data id /// Deletes a data log by data id
fileprivate func _cleanupDataLog(dataId: String) { fileprivate func _cleanupDataLog(dataId: String) {
let logs = self._dataLogs.filter { $0.dataId == dataId } let logs = self._deleteLogs.filter { $0.dataId == dataId }
self._dataLogs.delete(contentOfs: logs) self._deleteLogs.delete(contentOfs: logs)
} }
/// Creates a delete log for an instance /// Creates a delete log for an instance
@ -816,7 +822,7 @@ public class StoreCenter {
fileprivate func _addDataLog<T: Storable>(_ instance: T, method: HTTPMethod) { fileprivate func _addDataLog<T: Storable>(_ instance: T, method: HTTPMethod) {
let dataLog = DataLog( let dataLog = DataLog(
dataId: instance.stringId, modelName: String(describing: T.self), operation: method) dataId: instance.stringId, modelName: String(describing: T.self), operation: method)
self._dataLogs.addOrUpdate(instance: dataLog) self._deleteLogs.addOrUpdate(instance: dataLog)
} }
// MARK: - Miscellanous // MARK: - Miscellanous
@ -863,7 +869,7 @@ public class StoreCenter {
/// This method triggers the framework to save and send failed api calls /// This method triggers the framework to save and send failed api calls
public func logsFailedAPICalls() { 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 /// 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 /// Returns the collection hosting an instance
func collectionOfInstance<T: Storable>(_ instance: T) -> BaseCollection<T>? { func collectionOfInstance<T: Storable>(_ instance: T) -> BaseCollection<T>? {
do { do {
let collection: BaseCollection<T> = try Store.main.collection() let collection: BaseCollection<T> = try self.mainStore.collection()
if collection.findById(instance.id) != nil { if collection.findById(instance.id) != nil {
return collection return collection
} else { } else {
@ -1028,7 +1034,7 @@ public class StoreCenter {
if let logs = self._logs { if let logs = self._logs {
return logs return logs
} else { } else {
let logsCollection: SyncedCollection<Log> = Store.main.registerSynchronizedCollection(limit: 50) let logsCollection: SyncedCollection<Log> = self.mainStore.registerSynchronizedCollection(limit: 50)
self._logs = logsCollection self._logs = logsCollection
return logsCollection return logsCollection
} }

@ -16,7 +16,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Returns a dummy SyncedCollection instance /// Returns a dummy SyncedCollection instance
public static func placeholder() -> SyncedCollection<T> { public static func placeholder() -> SyncedCollection<T> {
return SyncedCollection<T>() return SyncedCollection<T>(store: Store(storeCenter: StoreCenter.main))
} }
/// Migrates if necessary and asynchronously decodes the json file /// Migrates if necessary and asynchronously decodes the json file
@ -50,7 +50,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
throw StoreError.cannotSyncCollection(name: self.resourceName) throw StoreError.cannotSyncCollection(name: self.resourceName)
} }
do { 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 { } catch {
Logger.error(error) Logger.error(error)
} }
@ -211,7 +211,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
if self.deleteItem(instance, shouldBeSynchronized: true) { if self.deleteItem(instance, shouldBeSynchronized: true) {
deleted.append(instance) deleted.append(instance)
} }
StoreCenter.main.createDeleteLog(instance) self.storeCenter.createDeleteLog(instance)
} }
let batch = OperationBatch<T>() let batch = OperationBatch<T>()
@ -254,7 +254,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Deletes an instance without writing, logs the operation and sends an API call /// Deletes an instance without writing, logs the operation and sends an API call
fileprivate func _deleteNoWrite(instance: T) { fileprivate func _deleteNoWrite(instance: T) {
self.deleteItem(instance, shouldBeSynchronized: true) self.deleteItem(instance, shouldBeSynchronized: true)
StoreCenter.main.createDeleteLog(instance) self.storeCenter.createDeleteLog(instance)
// await self._sendDeletion(instance) // await self._sendDeletion(instance)
} }
@ -324,7 +324,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) async { fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) async {
do { do {
try await StoreCenter.main.sendOperationBatch(batch) try await self.storeCenter.sendOperationBatch(batch)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -332,7 +332,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
fileprivate func _executeBatchOnce(_ batch: OperationBatch<T>) async { fileprivate func _executeBatchOnce(_ batch: OperationBatch<T>) async {
do { do {
try await StoreCenter.main.singleBatchExecution(batch) try await self.storeCenter.singleBatchExecution(batch)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -341,13 +341,13 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
// MARK: Single calls // MARK: Single calls
public func addsIfPostSucceeds(_ instance: T) async throws { 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) self.addOrUpdateNoSync(result)
} }
} }
public func updateIfPutSucceeds(_ instance: T) async throws { 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) self.addOrUpdateNoSync(result)
} }
} }
@ -422,11 +422,13 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
// MARK: - Others // MARK: - Others
/// Sends a POST request for the instance, and changes the collection to perform a write /// Sends a POST request for the instance, and changes the collection to perform a write
public func writeChangeAndInsertOnServer(instance: T) async { public func writeChangeAndInsertOnServer(instance: T) {
defer { Task {
self.setChanged() await self._sendInsertion(instance)
await MainActor.run {
self.setChanged()
}
} }
await self._sendInsertion(instance)
} }
} }

@ -23,7 +23,7 @@ public protocol SyncedStorable: Storable {
} }
protocol URLParameterConvertible { protocol URLParameterConvertible {
func queryParameters() -> [String : String] func queryParameters(storeCenter: StoreCenter) -> [String : String]
} }
public protocol SideStorable { public protocol SideStorable {

@ -11,6 +11,8 @@ import Combine
class WebSocketManager: ObservableObject { class WebSocketManager: ObservableObject {
fileprivate(set) var storeCenter: StoreCenter
fileprivate var _webSocketTask: URLSessionWebSocketTask? fileprivate var _webSocketTask: URLSessionWebSocketTask?
fileprivate var _timer: Timer? fileprivate var _timer: Timer?
fileprivate var _url: String fileprivate var _url: String
@ -19,7 +21,8 @@ class WebSocketManager: ObservableObject {
fileprivate var _failure = false fileprivate var _failure = false
fileprivate var _pingOk = false fileprivate var _pingOk = false
init(urlString: String) { init(storeCenter: StoreCenter, urlString: String) {
self.storeCenter = storeCenter
self._url = urlString self._url = urlString
_setupWebSocket() _setupWebSocket()
} }
@ -63,13 +66,13 @@ class WebSocketManager: ObservableObject {
switch message { switch message {
case .string(let deviceId): case .string(let deviceId):
// print("device id = \(StoreCenter.main.deviceId()), origin id: \(deviceId)") // print("device id = \(StoreCenter.main.deviceId()), origin id: \(deviceId)")
guard StoreCenter.main.deviceId() != deviceId else { guard self.storeCenter.deviceId() != deviceId else {
break break
} }
Task { Task {
do { do {
try await StoreCenter.main.synchronizeLastUpdates() try await self.storeCenter.synchronizeLastUpdates()
} catch { } catch {
Logger.error(error) Logger.error(error)
} }

@ -34,7 +34,7 @@ class Thing: SyncedModelObject, SyncedStorable, URLParameterConvertible {
static func relationships() -> [LeStorage.Relationship] { return [] } static func relationships() -> [LeStorage.Relationship] { return [] }
func queryParameters() -> [String : String] { func queryParameters(storeCenter: StoreCenter) -> [String : String] {
return ["yeah?" : "god!"] return ["yeah?" : "god!"]
} }
@ -43,7 +43,7 @@ class Thing: SyncedModelObject, SyncedStorable, URLParameterConvertible {
struct ApiCallTests { struct ApiCallTests {
@Test func testApiCallProvisioning1() async throws { @Test func testApiCallProvisioning1() async throws {
let collection = ApiCallCollection<Thing>() let collection = ApiCallCollection<Thing>(storeCenter: StoreCenter.main)
let thing = Thing(name: "yeah") let thing = Thing(name: "yeah")
@ -69,7 +69,7 @@ struct ApiCallTests {
} }
@Test func testApiCallProvisioning2() async throws { @Test func testApiCallProvisioning2() async throws {
let collection = ApiCallCollection<Thing>() let collection = ApiCallCollection<Thing>(storeCenter: StoreCenter.main)
let thing = Thing(name: "yeah") let thing = Thing(name: "yeah")
@ -94,7 +94,7 @@ struct ApiCallTests {
} }
@Test func testApiCallProvisioning3() async throws { @Test func testApiCallProvisioning3() async throws {
let collection = ApiCallCollection<Thing>() let collection = ApiCallCollection<Thing>(storeCenter: StoreCenter.main)
let thing = Thing(name: "yeah") let thing = Thing(name: "yeah")
@ -107,7 +107,7 @@ struct ApiCallTests {
} }
@Test func testGetProvisioning() async throws { @Test func testGetProvisioning() async throws {
let collection = ApiCallCollection<Thing>() let collection = ApiCallCollection<Thing>(storeCenter: StoreCenter.main)
try await collection.sendGetRequest(storeId: "1") try await collection.sendGetRequest(storeId: "1")
await #expect(collection.items.count == 1) await #expect(collection.items.count == 1)

@ -6,7 +6,7 @@
// //
import Testing import Testing
import LeStorage @testable import LeStorage
class Car: ModelObject, Storable { class Car: ModelObject, Storable {
@ -47,8 +47,9 @@ struct CollectionsTests {
var boats: SyncedCollection<Boat> var boats: SyncedCollection<Boat>
init() { init() {
cars = Store.main.registerCollection(inMemory: true) let storeCenter = StoreCenter.main
boats = Store.main.registerSynchronizedCollection(inMemory: true) cars = storeCenter.mainStore.registerCollection(inMemory: true)
boats = storeCenter.mainStore.registerSynchronizedCollection(inMemory: true)
} }
func ensureCollectionLoaded(_ collection: any SomeCollection) async throws { func ensureCollectionLoaded(_ collection: any SomeCollection) async throws {

@ -6,7 +6,7 @@
// //
import Testing import Testing
import LeStorage @testable import LeStorage
class IntObject: ModelObject, Storable { class IntObject: ModelObject, Storable {
@ -32,7 +32,6 @@ class IntObject: ModelObject, Storable {
class StringObject: ModelObject, Storable { class StringObject: ModelObject, Storable {
static func resourceName() -> String { "string" } static func resourceName() -> String { "string" }
static func tokenExemptedMethods() -> [LeStorage.HTTPMethod] { [] } static func tokenExemptedMethods() -> [LeStorage.HTTPMethod] { [] }
static var relationshipNames: [String] = []
var id: String var id: String
var name: String var name: String
@ -56,8 +55,9 @@ struct IdentifiableTests {
let stringObjects: StoredCollection<StringObject> let stringObjects: StoredCollection<StringObject>
init() { init() {
intObjects = Store.main.registerCollection() let storeCenter = StoreCenter.main
stringObjects = Store.main.registerCollection() intObjects = storeCenter.mainStore.registerCollection()
stringObjects = storeCenter.mainStore.registerCollection()
} }
func ensureCollectionLoaded(_ collection: any SomeCollection) async throws { func ensureCollectionLoaded(_ collection: any SomeCollection) async throws {

@ -6,7 +6,7 @@
// //
import Testing import Testing
import LeStorage @testable import LeStorage
struct Error: Swift.Error, CustomStringConvertible { struct Error: Swift.Error, CustomStringConvertible {
let description: String let description: String
@ -21,7 +21,7 @@ struct StoredCollectionTests {
var collection: StoredCollection<MockStorable> var collection: StoredCollection<MockStorable>
init() { init() {
collection = Store.main.registerCollection() collection = StoreCenter.main.mainStore.registerCollection()
} }
func ensureCollectionLoaded() async throws { func ensureCollectionLoaded() async throws {

Loading…
Cancel
Save