refactor the server into local copy system + doc

sync2
Laurent 10 months ago
parent 97d8405732
commit 5a112f8c44
  1. 13
      LeStorage/ApiCallCollection.swift
  2. 8
      LeStorage/ModelObject.swift
  3. 3
      LeStorage/Relationship.swift
  4. 26
      LeStorage/Services.swift
  5. 5
      LeStorage/Storable.swift
  6. 25
      LeStorage/StoreCenter.swift
  7. 44
      LeStorage/StoredCollection+Sync.swift
  8. 9
      LeStorage/SyncedStorable.swift

@ -190,7 +190,10 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
if batch.count == 1, let apiCall = batch.first, apiCall.method == .get { if batch.count == 1, let apiCall = batch.first, apiCall.method == .get {
let _: Empty = try await self._executeGetCall(apiCall) let _: Empty = try await self._executeGetCall(apiCall)
} else { } else {
try await self._executeApiCalls(batch) let success = try await self._executeApiCalls(batch)
if T.copyServerResponse {
StoreCenter.main.updateLocalInstances(success)
}
} }
} catch { } catch {
Logger.error(error) Logger.error(error)
@ -313,7 +316,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
} }
} }
func executeBatch(_ batch: BatchPreparation<T>) async throws { func executeBatch(_ batch: OperationBatch<T>) async throws -> [T] {
var apiCalls: [ApiCall<T>] = [] var apiCalls: [ApiCall<T>] = []
let transactionId = Store.randomId() let transactionId = Store.randomId()
@ -332,7 +335,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
self._prepareCall(apiCall: call) self._prepareCall(apiCall: call)
apiCalls.append(call) apiCalls.append(call)
} }
try await self._executeApiCalls(apiCalls) return try await self._executeApiCalls(apiCalls)
} }
/// Sends an insert api call for the provided [instance] /// Sends an insert api call for the provided [instance]
@ -396,8 +399,8 @@ 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 { fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws -> [T] {
try await StoreCenter.main.execute(apiCalls: apiCalls) return try await StoreCenter.main.execute(apiCalls: apiCalls)
} }
/// Returns the content of the API call file as a String /// Returns the content of the API call file as a String

@ -19,10 +19,6 @@ open class ModelObject: NSObject {
} }
open func copyFromServerInstance(_ instance: any Storable) -> Bool {
return false
}
static var relationshipNames: [String] = [] static var relationshipNames: [String] = []
} }
@ -57,6 +53,10 @@ open class SyncedModelObject: BaseModelObject {
public var lastUpdate: Date = Date() public var lastUpdate: Date = Date()
public var shared: Bool? public var shared: Bool?
open func copyFromServerInstance(_ instance: any Storable) -> Bool {
return false
}
public override init() { public override init() {
super.init() super.init()
} }

@ -12,6 +12,9 @@ public struct Relationship {
self.keyPath = keyPath self.keyPath = keyPath
} }
/// The type of the relationship
var type: any Storable.Type var type: any Storable.Type
/// the keyPath to access the relationship
var keyPath: AnyKeyPath var keyPath: AnyKeyPath
} }

@ -40,6 +40,9 @@ 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 {
/// The base API URL to send requests
fileprivate(set) var baseURL: String
/// A KeychainStore object used to store the user's token /// A KeychainStore object used to store the user's token
let keychainStore: KeychainStore let keychainStore: KeychainStore
@ -47,13 +50,8 @@ public class Services {
self.baseURL = url self.baseURL = url
self.keychainStore = KeychainStore(serverId: url) self.keychainStore = KeychainStore(serverId: url)
Logger.log("create keystore with id: \(url)") Logger.log("create keystore with id: \(url)")
} }
/// The base API URL to send requests
fileprivate(set) var baseURL: String
// MARK: - Base // MARK: - Base
/// Runs a request using a configuration object /// Runs a request using a configuration object
@ -61,8 +59,7 @@ public class Services {
/// - serviceConf: A instance of ServiceConf /// - serviceConf: A instance of ServiceConf
/// - apiCallId: an optional id referencing an ApiCall /// - apiCallId: an optional id referencing an ApiCall
fileprivate func _runRequest<U: Decodable>(serviceCall: ServiceCall) fileprivate func _runRequest<U: Decodable>(serviceCall: ServiceCall)
async throws -> U async throws -> U {
{
let request = try self._baseRequest(call: serviceCall) let request = try self._baseRequest(call: serviceCall)
return try await _runRequest(request) return try await _runRequest(request)
} }
@ -84,13 +81,14 @@ public class Services {
/// - request: the URLRequest to run /// - request: the URLRequest to run
/// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure
fileprivate func _runSyncPostRequest<T: SyncedStorable>( fileprivate func _runSyncPostRequest<T: SyncedStorable>(
_ request: URLRequest, type: T.Type) async throws { _ request: URLRequest, type: T.Type) async throws -> [T] {
let debugURL = request.url?.absoluteString ?? "" let debugURL = request.url?.absoluteString ?? ""
// print("Run \(request.httpMethod ?? "") \(debugURL)") // print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
print("sync POST \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")") print("sync POST \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")")
var rescheduleApiCalls: Bool = false var rescheduleApiCalls: Bool = false
var success: [T] = []
if let response = task.1 as? HTTPURLResponse { if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode let statusCode = response.statusCode
@ -100,8 +98,12 @@ public class Services {
let decoded: BatchResponse<T> = try self._decode(data: task.0) let decoded: BatchResponse<T> = try self._decode(data: task.0)
for result in decoded.results { for result in decoded.results {
switch result.status { switch result.status {
case 200..<300: case 200..<300:
if let data = result.data {
success.append(data)
}
try await StoreCenter.main.deleteApiCallById(type: T.self, id: result.apiCallId) try await StoreCenter.main.deleteApiCallById(type: T.self, id: result.apiCallId)
default: default:
@ -133,8 +135,10 @@ public class Services {
} }
if rescheduleApiCalls { if rescheduleApiCalls {
try await StoreCenter.main.rescheduleApiCalls(type: T.self) try? await StoreCenter.main.rescheduleApiCalls(type: T.self)
} }
return success
} }
/// Runs a request using a traditional URLRequest /// Runs a request using a traditional URLRequest
@ -496,9 +500,9 @@ public class Services {
} }
/// Executes an ApiCall /// Executes an ApiCall
func runApiCalls<T: SyncedStorable>(_ apiCalls: [ApiCall<T>]) async throws { func runApiCalls<T: SyncedStorable>(_ apiCalls: [ApiCall<T>]) async throws -> [T] {
let request = try self._syncPostRequest(from: apiCalls) let request = try self._syncPostRequest(from: apiCalls)
try await self._runSyncPostRequest(request, type: T.self) return try await self._runSyncPostRequest(request, type: T.self)
} }
/// Returns the URLRequest for an ApiCall /// Returns the URLRequest for an ApiCall

@ -18,13 +18,16 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
static func resourceName() -> String static func resourceName() -> String
/// A method that deletes the local dependencies of the resource /// A method that deletes the local dependencies of the resource
/// Mimics the behavior the cascading delete on the django server /// Mimics the behavior of the cascading delete on the django server
/// Typically when we delete a resource, we automatically delete items that depends on it, /// Typically when we delete a resource, we automatically delete items that depends on it,
/// so when we do that on the server, we also need to do it locally /// so when we do that on the server, we also need to do it locally
func deleteDependencies() func deleteDependencies()
/// Copies the content of another item into the instance
/// This behavior has been made to get live updates when looking at properties in SwiftUI screens
func copy(from other: any Storable) func copy(from other: any Storable)
/// This method returns RelationShips objects of the type
static func relationships() -> [Relationship] static func relationships() -> [Relationship]
} }

@ -365,8 +365,8 @@ public class StoreCenter {
} }
/// Executes an API call /// Executes an API call
func execute<T: SyncedStorable>(apiCalls: [ApiCall<T>]) async throws { func execute<T: SyncedStorable>(apiCalls: [ApiCall<T>]) async throws -> [T] {
try await self.service().runApiCalls(apiCalls) return try await self.service().runApiCalls(apiCalls)
} }
// MARK: - Api calls // MARK: - Api calls
@ -377,13 +377,11 @@ public class StoreCenter {
&& self.userIsAllowed() && self.userIsAllowed()
} }
func prepareOperationBatch<T: SyncedStorable>(_ batch: BatchPreparation<T>) { func sendOperationBatch<T: SyncedStorable>(_ batch: OperationBatch<T>) async throws -> [T] {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return return []
}
Task {
try await self.apiCallCollection().executeBatch(batch)
} }
return try await self.apiCallCollection().executeBatch(batch)
} }
/// Transmit the insertion request to the ApiCall collection /// Transmit the insertion request to the ApiCall collection
@ -836,13 +834,24 @@ public class StoreCenter {
// MARK: - Instant update // MARK: - Instant update
/// Updates a local object with a server instance /// Updates a local object with a server instance
func updateFromServerInstance<T: SyncedStorable>(_ result: T) { func updateLocalInstances<T: SyncedStorable>(_ results: [T]) {
for result in results {
if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) { if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) {
if storedCollection.findById(result.id) != nil { if storedCollection.findById(result.id) != nil {
storedCollection.updateFromServerInstance(result) storedCollection.updateFromServerInstance(result)
} }
} }
} }
}
/// Updates a local object with a server instance
// func updateFromServerInstance<T: SyncedStorable>(_ result: T) {
// if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) {
// if storedCollection.findById(result.id) != nil {
// storedCollection.updateFromServerInstance(result)
// }
// }
// }
/// Returns the collection hosting an instance /// Returns the collection hosting an instance
func collectionOfInstance<T: Storable>(_ instance: T) -> StoredCollection<T>? { func collectionOfInstance<T: Storable>(_ instance: T) -> StoredCollection<T>? {

@ -54,10 +54,12 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
func updateFromServerInstance(_ serverInstance: T) { func updateFromServerInstance(_ serverInstance: T) {
DispatchQueue.main.async { DispatchQueue.main.async {
if let localInstance = self.findById(serverInstance.id) { if let localInstance = self.findById(serverInstance.id) {
let modified = localInstance.copyFromServerInstance(serverInstance) localInstance.copy(from: serverInstance)
if modified {
self.setChanged() self.setChanged()
} // let modified = localInstance.copyFromServerInstance(serverInstance)
// if modified {
// self.setChanged()
// }
} }
} }
} }
@ -116,7 +118,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
} }
let date = Date() let date = Date()
let batch = BatchPreparation<T>() let batch = OperationBatch<T>()
for instance in sequence { for instance in sequence {
instance.lastUpdate = date instance.lastUpdate = date
@ -131,7 +133,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
} }
} }
self._prepareBatch(batch) self._sendOperationBatch(batch)
} }
@ -152,9 +154,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
StoreCenter.main.createDeleteLog(instance) StoreCenter.main.createDeleteLog(instance)
} }
let batch = BatchPreparation<T>() let batch = OperationBatch<T>()
batch.deletes = sequence batch.deletes = Array(sequence)
self._prepareBatch(batch) self._sendOperationBatch(batch)
} }
/// Deletes an instance and writes /// Deletes an instance and writes
@ -176,19 +178,28 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
// MARK: - Send requests // MARK: - Send requests
fileprivate func _sendInsertion(_ instance: T) { fileprivate func _sendInsertion(_ instance: T) {
self._prepareBatch(BatchPreparation(insert: instance)) self._sendOperationBatch(OperationBatch(insert: instance))
} }
fileprivate func _sendUpdate(_ instance: T) { fileprivate func _sendUpdate(_ instance: T) {
self._prepareBatch(BatchPreparation(update: instance)) self._sendOperationBatch(OperationBatch(update: instance))
} }
fileprivate func _sendDeletion(_ instance: T) { fileprivate func _sendDeletion(_ instance: T) {
self._prepareBatch(BatchPreparation(delete: instance)) self._sendOperationBatch(OperationBatch(delete: instance))
} }
fileprivate func _prepareBatch(_ batch: BatchPreparation<T>) { fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) {
StoreCenter.main.prepareOperationBatch(batch) Task {
do {
let success = try await StoreCenter.main.sendOperationBatch(batch)
for item in success {
self.updateFromServerInstance(item)
}
} catch {
Logger.error(error)
}
}
} }
/// Sends an insert api call for the provided /// Sends an insert api call for the provided
@ -268,10 +279,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
} }
class BatchPreparation<T> { class OperationBatch<T> {
var inserts: [T] = [] var inserts: [T] = []
var updates: [T] = [] var updates: [T] = []
var deletes: any Sequence<T> = [] var deletes: [T] = []
init() { init() {
@ -292,4 +303,7 @@ class BatchPreparation<T> {
func addUpdate(_ instance: T) { func addUpdate(_ instance: T) {
self.updates.append(instance) self.updates.append(instance)
} }
func addDelete(_ instance: T) {
self.deletes.append(instance)
}
} }

@ -15,11 +15,8 @@ public protocol SyncedStorable: Storable {
/// Returns HTTP methods that do not need to pass the token to the request /// Returns HTTP methods that do not need to pass the token to the request
static func tokenExemptedMethods() -> [HTTPMethod] static func tokenExemptedMethods() -> [HTTPMethod]
/// A method called to retrieve data added by the server on a POST request /// Returns whether we should copy the server response into the local instance
/// The method will be called after a POST has succeeded, static var copyServerResponse: Bool { get }
/// and will provide a copy of what's on the server
/// Should return true to trigger a write on the collection, or false if nothing changed
func copyFromServerInstance(_ instance: any Storable) -> Bool
} }
@ -33,6 +30,8 @@ public protocol SideStorable {
extension SyncedStorable { extension SyncedStorable {
public static var copyServerResponse: Bool { return false }
func getStoreId() -> String? { func getStoreId() -> String? {
if let alt = self as? SideStorable { if let alt = self as? SideStorable {
return alt.storeId return alt.storeId

Loading…
Cancel
Save