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. 28
      LeStorage/Services.swift
  5. 5
      LeStorage/Storable.swift
  6. 31
      LeStorage/StoreCenter.swift
  7. 46
      LeStorage/StoredCollection+Sync.swift
  8. 11
      LeStorage/SyncedStorable.swift

@ -190,7 +190,10 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
if batch.count == 1, let apiCall = batch.first, apiCall.method == .get {
let _: Empty = try await self._executeGetCall(apiCall)
} else {
try await self._executeApiCalls(batch)
let success = try await self._executeApiCalls(batch)
if T.copyServerResponse {
StoreCenter.main.updateLocalInstances(success)
}
}
} catch {
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>] = []
let transactionId = Store.randomId()
@ -332,7 +335,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
self._prepareCall(apiCall: call)
apiCalls.append(call)
}
try await self._executeApiCalls(apiCalls)
return try await self._executeApiCalls(apiCalls)
}
/// Sends an insert api call for the provided [instance]
@ -396,8 +399,8 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Executes an API call
/// For POST requests, potentially copies additional data coming from the server during the insert
fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws {
try await StoreCenter.main.execute(apiCalls: apiCalls)
fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws -> [T] {
return try await StoreCenter.main.execute(apiCalls: apiCalls)
}
/// 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] = []
}
@ -57,6 +53,10 @@ open class SyncedModelObject: BaseModelObject {
public var lastUpdate: Date = Date()
public var shared: Bool?
open func copyFromServerInstance(_ instance: any Storable) -> Bool {
return false
}
public override init() {
super.init()
}

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

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

@ -18,13 +18,16 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
static func resourceName() -> String
/// 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,
/// so when we do that on the server, we also need to do it locally
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)
/// This method returns RelationShips objects of the type
static func relationships() -> [Relationship]
}

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

@ -54,10 +54,12 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
func updateFromServerInstance(_ serverInstance: T) {
DispatchQueue.main.async {
if let localInstance = self.findById(serverInstance.id) {
let modified = localInstance.copyFromServerInstance(serverInstance)
if modified {
self.setChanged()
}
localInstance.copy(from: serverInstance)
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 batch = BatchPreparation<T>()
let batch = OperationBatch<T>()
for instance in sequence {
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)
}
let batch = BatchPreparation<T>()
batch.deletes = sequence
self._prepareBatch(batch)
let batch = OperationBatch<T>()
batch.deletes = Array(sequence)
self._sendOperationBatch(batch)
}
/// Deletes an instance and writes
@ -176,19 +178,28 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
// MARK: - Send requests
fileprivate func _sendInsertion(_ instance: T) {
self._prepareBatch(BatchPreparation(insert: instance))
self._sendOperationBatch(OperationBatch(insert: instance))
}
fileprivate func _sendUpdate(_ instance: T) {
self._prepareBatch(BatchPreparation(update: instance))
self._sendOperationBatch(OperationBatch(update: instance))
}
fileprivate func _sendDeletion(_ instance: T) {
self._prepareBatch(BatchPreparation(delete: instance))
self._sendOperationBatch(OperationBatch(delete: instance))
}
fileprivate func _prepareBatch(_ batch: BatchPreparation<T>) {
StoreCenter.main.prepareOperationBatch(batch)
fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) {
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
@ -268,10 +279,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
}
class BatchPreparation<T> {
class OperationBatch<T> {
var inserts: [T] = []
var updates: [T] = []
var deletes: any Sequence<T> = []
var deletes: [T] = []
init() {
@ -292,4 +303,7 @@ class BatchPreparation<T> {
func addUpdate(_ instance: T) {
self.updates.append(instance)
}
func addDelete(_ instance: T) {
self.deletes.append(instance)
}
}

@ -15,12 +15,9 @@ public protocol SyncedStorable: Storable {
/// Returns HTTP methods that do not need to pass the token to the request
static func tokenExemptedMethods() -> [HTTPMethod]
/// A method called to retrieve data added by the server on a POST request
/// The method will be called after a POST has succeeded,
/// 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
/// Returns whether we should copy the server response into the local instance
static var copyServerResponse: Bool { get }
}
protocol URLParameterConvertible {
@ -33,6 +30,8 @@ public protocol SideStorable {
extension SyncedStorable {
public static var copyServerResponse: Bool { return false }
func getStoreId() -> String? {
if let alt = self as? SideStorable {
return alt.storeId

Loading…
Cancel
Save