multistore
Laurent 2 years ago
parent 9cfa848a11
commit cc0c8db826
  1. 4
      LeStorage.xcodeproj/project.pbxproj
  2. 42
      LeStorage/ApiCall.swift
  3. 50
      LeStorage/Services.swift
  4. 2
      LeStorage/Storable.swift
  5. 104
      LeStorage/Store.swift
  6. 23
      LeStorage/StoredCollection.swift

@ -18,6 +18,7 @@
C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */; }; C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */; };
C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D602B6D3C1300ADC637 /* Services.swift */; }; C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D602B6D3C1300ADC637 /* Services.swift */; };
C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D642B6E92FE00ADC637 /* Storable.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -43,6 +44,7 @@
C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; }; C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
C4A47D602B6D3C1300ADC637 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; }; C4A47D602B6D3C1300ADC637 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
C4A47D642B6E92FE00ADC637 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = "<group>"; }; C4A47D642B6E92FE00ADC637 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = "<group>"; };
C4A47D662B6FF83A00ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -88,6 +90,7 @@
C425D4372B6D24E1002A7B48 /* LeStorage.h */, C425D4372B6D24E1002A7B48 /* LeStorage.h */,
C425D4382B6D24E1002A7B48 /* LeStorage.docc */, C425D4382B6D24E1002A7B48 /* LeStorage.docc */,
C4A47D602B6D3C1300ADC637 /* Services.swift */, C4A47D602B6D3C1300ADC637 /* Services.swift */,
C4A47D662B6FF83A00ADC637 /* ApiCall.swift */,
C425D4572B6D2519002A7B48 /* Store.swift */, C425D4572B6D2519002A7B48 /* Store.swift */,
C4A47D642B6E92FE00ADC637 /* Storable.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */,
C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */,
@ -230,6 +233,7 @@
C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */,
C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */,
C4A47D672B6FF83A00ADC637 /* ApiCall.swift in Sources */,
C425D4582B6D2519002A7B48 /* Store.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

@ -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<T : Storable> : 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)
}
}
}

@ -36,11 +36,20 @@ class Services {
// MARK: - Base // MARK: - Base
fileprivate func runRequest<T : Decodable>(_ request: URLRequest) async throws -> T { fileprivate func runRequest<T : Decodable>(_ request: URLRequest, apiCallId: String? = nil) async throws -> T {
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
if let response = task.1 as? HTTPURLResponse { if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode 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)") Logger.log("status code = \(statusCode)")
} }
@ -78,26 +87,45 @@ class Services {
// MARK: - Services // MARK: - Services
func get<T : Storable>() async throws -> [T] { func get<T : Storable>() async throws -> [T] {
let getRequest = try getRequest(servicePath: T.resourceName + "/") let getRequest = try getRequest(servicePath: T.resourceName() + "/")
return try await self.runRequest(getRequest) return try await self.runRequest(getRequest)
} }
func insert<T : Storable>(_ instance: T) async throws -> T { func insert<T : Storable>(_ instance: T) async throws -> T {
var postRequest = try postRequest(servicePath: T.resourceName + "/") let apiCall = try self._createCall(method: Method.post, instance: instance)
postRequest.httpBody = try instance.jsonData() return try await self.runApiCall(apiCall)
return try await self.runRequest(postRequest)
} }
func update<T : Storable>(_ instance: T) async throws -> T { func update<T : Storable>(_ instance: T) async throws -> T {
var putRequest = try putRequest(servicePath: T.resourceName + "/") let apiCall = try self._createCall(method: Method.put, instance: instance)
putRequest.httpBody = try instance.jsonData() return try await self.runApiCall(apiCall)
return try await self.runRequest(putRequest)
} }
func delete<T : Storable>(_ instance: T) async throws -> T { func delete<T : Storable>(_ instance: T) async throws -> T {
var deleteRequest = try deleteRequest(servicePath: T.resourceName + "/") let apiCall = try self._createCall(method: Method.delete, instance: instance)
deleteRequest.httpBody = try instance.jsonData() return try await self.runApiCall(apiCall)
return try await self.runRequest(deleteRequest) }
fileprivate func _createCall<T : Storable>(method: Method, instance: T) throws -> ApiCall<T> {
let data = try instance.jsonData()
let url = self._baseURL + T.resourceName() + "/"
return ApiCall(url: url, method: method.rawValue, body: data)
}
func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) 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<T : Storable>(from apiCall: ApiCall<T>) 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
} }
} }

@ -8,7 +8,7 @@
import Foundation import Foundation
public protocol Storable : Codable, Identifiable where ID : StringProtocol { public protocol Storable : Codable, Identifiable where ID : StringProtocol {
static var resourceName: String { get } static func resourceName() -> String
} }
extension Storable { extension Storable {

@ -7,6 +7,13 @@
import Foundation import Foundation
enum StoreError: Error {
case missingService
case unexpectedCollectionType(name: String)
case apiCallCollectionNotRegistered(type: String)
case collectionNotRegistered(type: String)
}
public class Store { public class Store {
public static let main = Store() public static let main = Store()
@ -24,13 +31,26 @@ public class Store {
} }
fileprivate var _services: Services? 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 init() { }
public func registerCollection<T : Storable>(synchronized: Bool) -> StoredCollection<T> { public func registerCollection<T : Storable>(synchronized: Bool) -> StoredCollection<T> {
// register collection
let collection = StoredCollection<T>(synchronized: synchronized, store: self) let collection = StoredCollection<T>(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<ApiCall<T>>(synchronized: false, store: self)
self._apiCallsCollections[T.resourceName()] = apiCallCollection
}
return collection return collection
} }
@ -39,11 +59,87 @@ public class Store {
} }
func findById<T : Storable>(_ id: String) -> T? { func findById<T : Storable>(_ id: String) -> T? {
guard let collection = self.collections[T.resourceName] as? StoredCollection<T> else { guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName) not registered") Logger.w("Collection \(T.resourceName()) not registered")
return nil return nil
} }
return collection.findById(id) return collection.findById(id)
} }
// MARK: - Api call rescheduling
func apiCallCollection<T : Storable>() throws -> StoredCollection<ApiCall<T>> {
if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection<ApiCall<T>> {
return apiCallCollection
}
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
func registerApiCall<T : Storable>(_ apiCall: ApiCall<T>) throws {
let collection: StoredCollection<ApiCall<T>> = try self.apiCallCollection()
collection.addOrUpdate(instance: apiCall)
}
func deleteApiCallById<T : Storable> (_ id: String, type: T.Type) throws {
let collection: StoredCollection<ApiCall<T>> = 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<T : Storable> (_ apiCall: ApiCall<T>) throws {
let collection: StoredCollection<ApiCall<T>> = 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<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T {
guard let service else {
throw StoreError.missingService
}
return try await service.runApiCall(apiCall)
}
func execute<T>(apiCall: ApiCall<T>) async throws {
_ = try await self._executeApiCall(apiCall)
}
} }

@ -8,7 +8,8 @@
import Foundation import Foundation
protocol SomeCollection : Identifiable { protocol SomeCollection : Identifiable {
func allItems() -> [any Storable]
func deleteById(_ id: String)
} }
public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollection, ObservableObject { public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollection, ObservableObject {
@ -22,7 +23,6 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
fileprivate var _hasChanged: Bool = false { fileprivate var _hasChanged: Bool = false {
didSet { didSet {
if self._hasChanged == true { if self._hasChanged == true {
self.objectWillChange.send()
self._scheduleWrite() self._scheduleWrite()
self._hasChanged = false self._hasChanged = false
} }
@ -36,7 +36,7 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} }
fileprivate var _fileName: String { fileprivate var _fileName: String {
return T.resourceName + ".json" return T.resourceName() + ".json"
} }
public func addOrUpdate(instance: T) { public func addOrUpdate(instance: T) {
@ -67,6 +67,18 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
return self.items.first(where: { $0.id == id }) 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 // MARK: - File access
fileprivate func _scheduleWrite() { fileprivate func _scheduleWrite() {
@ -106,7 +118,6 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
DispatchQueue.main.sync { DispatchQueue.main.sync {
Logger.log("loaded \(self._fileName) with \(decoded.count) items") Logger.log("loaded \(self._fileName) with \(decoded.count) items")
self.items = decoded self.items = decoded
self.objectWillChange.send()
} }
} }
@ -185,4 +196,8 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} }
} }
public func append(_ newElement: T) {
self.addOrUpdate(instance: newElement)
}
} }

Loading…
Cancel
Save