From cc0c8db8261fd1599d13d4fcec7b199fd77f868b Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 5 Feb 2024 14:57:37 +0100 Subject: [PATCH] Upgrade --- LeStorage.xcodeproj/project.pbxproj | 4 ++ LeStorage/ApiCall.swift | 42 +++++++++++ LeStorage/Services.swift | 52 ++++++++++---- LeStorage/Storable.swift | 2 +- LeStorage/Store.swift | 106 ++++++++++++++++++++++++++-- LeStorage/StoredCollection.swift | 23 ++++-- 6 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 LeStorage/ApiCall.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index dec56c7..6cd59d0 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */; }; C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D602B6D3C1300ADC637 /* Services.swift */; }; C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D642B6E92FE00ADC637 /* Storable.swift */; }; + C4A47D672B6FF83A00ADC637 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D662B6FF83A00ADC637 /* ApiCall.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,6 +44,7 @@ C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; C4A47D602B6D3C1300ADC637 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; C4A47D642B6E92FE00ADC637 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = ""; }; + C4A47D662B6FF83A00ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,6 +90,7 @@ C425D4372B6D24E1002A7B48 /* LeStorage.h */, C425D4382B6D24E1002A7B48 /* LeStorage.docc */, C4A47D602B6D3C1300ADC637 /* Services.swift */, + C4A47D662B6FF83A00ADC637 /* ApiCall.swift */, C425D4572B6D2519002A7B48 /* Store.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */, C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, @@ -230,6 +233,7 @@ C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, + C4A47D672B6FF83A00ADC637 /* ApiCall.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LeStorage/ApiCall.swift b/LeStorage/ApiCall.swift new file mode 100644 index 0000000..3e1a8b0 --- /dev/null +++ b/LeStorage/ApiCall.swift @@ -0,0 +1,42 @@ +// +// ApiCall.swift +// LeStorage +// +// Created by Laurent Morvillier on 04/02/2024. +// + +import Foundation + +protocol SomeCall : Storable { + func execute() throws +} + +struct ApiCall : Storable, SomeCall { + + static func resourceName() -> String { return "apicalls" } + + var id: String = UUID().uuidString + + /// The http URL of the call + var url: String + + /// The HTTP method of the call: post... + var method: String + + /// The content of the call + var body: Data + + /// The number of times the call has been executed + var attemptsCount = 1 + + /// The date of the last execution + var lastAttemptDate = Date() + + /// Executes the api call + func execute() throws { + Task { + try await Store.main.execute(apiCall: self) + } + } + +} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index d9c2d9c..e9874a6 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -36,11 +36,20 @@ class Services { // MARK: - Base - fileprivate func runRequest(_ request: URLRequest) async throws -> T { + fileprivate func runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode + switch statusCode { + case 200...300: + if let apiCallId, + let collectionName = (T.self as? any Storable.Type)?.resourceName() { + try Store.main.deleteApiCallById(apiCallId, collectionName: collectionName) + } + default: + Store.main.startCallsRescheduling() + } Logger.log("status code = \(statusCode)") } @@ -78,26 +87,45 @@ class Services { // MARK: - Services func get() async throws -> [T] { - let getRequest = try getRequest(servicePath: T.resourceName + "/") + let getRequest = try getRequest(servicePath: T.resourceName() + "/") return try await self.runRequest(getRequest) } func insert(_ instance: T) async throws -> T { - var postRequest = try postRequest(servicePath: T.resourceName + "/") - postRequest.httpBody = try instance.jsonData() - return try await self.runRequest(postRequest) + let apiCall = try self._createCall(method: Method.post, instance: instance) + return try await self.runApiCall(apiCall) } func update(_ instance: T) async throws -> T { - var putRequest = try putRequest(servicePath: T.resourceName + "/") - putRequest.httpBody = try instance.jsonData() - return try await self.runRequest(putRequest) + let apiCall = try self._createCall(method: Method.put, instance: instance) + return try await self.runApiCall(apiCall) } func delete(_ instance: T) async throws -> T { - var deleteRequest = try deleteRequest(servicePath: T.resourceName + "/") - deleteRequest.httpBody = try instance.jsonData() - return try await self.runRequest(deleteRequest) + let apiCall = try self._createCall(method: Method.delete, instance: instance) + return try await self.runApiCall(apiCall) } - + + fileprivate func _createCall(method: Method, instance: T) throws -> ApiCall { + let data = try instance.jsonData() + let url = self._baseURL + T.resourceName() + "/" + return ApiCall(url: url, method: method.rawValue, body: data) + } + + func runApiCall(_ apiCall: ApiCall) async throws -> T { + try Store.main.registerApiCall(apiCall) + let request = try self._request(from: apiCall) + return try await self.runRequest(request, apiCallId: apiCall.id) + } + + fileprivate func _request(from apiCall: ApiCall) throws -> URLRequest { + guard let url = URL(string: apiCall.url) else { + throw ServiceError.urlCreationError(url: apiCall.url) + } + var request = URLRequest(url: url) + request.httpMethod = apiCall.method + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + return request + } + } diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 7e43112..6293402 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -8,7 +8,7 @@ import Foundation public protocol Storable : Codable, Identifiable where ID : StringProtocol { - static var resourceName: String { get } + static func resourceName() -> String } extension Storable { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 2c9fadf..968be2b 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -7,6 +7,13 @@ import Foundation +enum StoreError: Error { + case missingService + case unexpectedCollectionType(name: String) + case apiCallCollectionNotRegistered(type: String) + case collectionNotRegistered(type: String) +} + public class Store { public static let main = Store() @@ -24,13 +31,26 @@ public class Store { } fileprivate var _services: Services? - fileprivate var collections: [String : any SomeCollection] = [:] + fileprivate var _collections: [String : any SomeCollection] = [:] + fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:] + + fileprivate var _apiCallsTimer: Timer? = nil + + fileprivate var _reschedulingCount: Int = 0 public init() { } - + public func registerCollection(synchronized: Bool) -> StoredCollection { + + // register collection let collection = StoredCollection(synchronized: synchronized, store: self) - self.collections[T.resourceName] = collection + self._collections[T.resourceName()] = collection + + if synchronized { // register additional collection for api calls + let apiCallCollection = StoredCollection>(synchronized: false, store: self) + self._apiCallsCollections[T.resourceName()] = apiCallCollection + } + return collection } @@ -39,11 +59,87 @@ public class Store { } func findById(_ id: String) -> T? { - guard let collection = self.collections[T.resourceName] as? StoredCollection else { - Logger.w("Collection \(T.resourceName) not registered") + guard let collection = self._collections[T.resourceName()] as? StoredCollection else { + Logger.w("Collection \(T.resourceName()) not registered") return nil } return collection.findById(id) } + // MARK: - Api call rescheduling + + func apiCallCollection() throws -> StoredCollection> { + if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection> { + return apiCallCollection + } + throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) + } + + func registerApiCall(_ apiCall: ApiCall) throws { + let collection: StoredCollection> = try self.apiCallCollection() + collection.addOrUpdate(instance: apiCall) + } + + func deleteApiCallById (_ id: String, type: T.Type) throws { + let collection: StoredCollection> = try self.apiCallCollection() + collection.deleteById(id) + } + + func deleteApiCallById(_ id: String, collectionName: String) throws { + if let collection = self._apiCallsCollections[collectionName] { + collection.deleteById(id) + return + } + throw StoreError.collectionNotRegistered(type: collectionName) + } + + func deleteApiCall (_ apiCall: ApiCall) throws { + let collection: StoredCollection> = try self.apiCallCollection() + collection.delete(instance: apiCall) + } + + func startCallsRescheduling() { + + self._reschedulingCount += 1 + + let delay = pow(2, 1 + self._reschedulingCount) + let seconds = NSDecimalNumber(decimal: delay).doubleValue + self._apiCallsTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { timer in + self._executeApiCalls() + }) + + } + + fileprivate func _executeApiCalls() { + + DispatchQueue(label: "lestorage.queue.network").async { + + do { + for collection in self._apiCallsCollections.values { + if let apiCalls = collection.allItems() as? [any SomeCall] { + for apiCall in apiCalls { + try apiCall.execute() + } + } else { + Logger.w("_apiCallsCollections item not castable to [any SomeCall] ") + } + } + } catch { + Logger.error(error) + } + } + + } + + fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { + guard let service else { + throw StoreError.missingService + } + return try await service.runApiCall(apiCall) + } + + func execute(apiCall: ApiCall) async throws { + _ = try await self._executeApiCall(apiCall) + } + } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 23fbd76..47a4479 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -8,7 +8,8 @@ import Foundation protocol SomeCollection : Identifiable { - + func allItems() -> [any Storable] + func deleteById(_ id: String) } public class StoredCollection : RandomAccessCollection, SomeCollection, ObservableObject { @@ -22,7 +23,6 @@ public class StoredCollection : RandomAccessCollection, SomeCollec fileprivate var _hasChanged: Bool = false { didSet { if self._hasChanged == true { - self.objectWillChange.send() self._scheduleWrite() self._hasChanged = false } @@ -36,7 +36,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } fileprivate var _fileName: String { - return T.resourceName + ".json" + return T.resourceName() + ".json" } public func addOrUpdate(instance: T) { @@ -67,6 +67,18 @@ public class StoredCollection : RandomAccessCollection, SomeCollec return self.items.first(where: { $0.id == id }) } + public func deleteById(_ id: String) { + if let instance = self.findById(id) { + self.delete(instance: instance) + } + } + + // MARK: - SomeCall + + func allItems() -> [any Storable] { + return self.items + } + // MARK: - File access fileprivate func _scheduleWrite() { @@ -106,7 +118,6 @@ public class StoredCollection : RandomAccessCollection, SomeCollec DispatchQueue.main.sync { Logger.log("loaded \(self._fileName) with \(decoded.count) items") self.items = decoded - self.objectWillChange.send() } } @@ -185,4 +196,8 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } } + public func append(_ newElement: T) { + self.addOrUpdate(instance: newElement) + } + }