sync2
Laurent 8 months ago
commit 7125d868ae
  1. 101
      LeStorage/ApiCallCollection.swift
  2. 2
      LeStorage/StoreCenter.swift
  3. 2
      LeStorage/StoredCollection+Sync.swift
  4. 2
      LeStorage/StoredCollection.swift
  5. 7
      LeStorageTests/ApiCallTests.swift

@ -22,7 +22,7 @@ protocol SomeCallCollection {
enum ApiCallError: Error, LocalizedError {
case encodingError(id: String, type: String)
var errorDescription: String? {
switch self {
case .encodingError(let id, let type):
@ -46,7 +46,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate var _isRescheduling: Bool = false
fileprivate var _schedulingTask: Task<(), Never>? = nil
/// Indicates whether the collection content has changed
/// Initiates a write when true
fileprivate var _hasChanged: Bool = false {
@ -57,7 +57,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
}
}
}
/// Starts the JSON file decoding synchronously or asynchronously
/// Reschedule Api calls if not empty
func loadFromFile() throws {
@ -157,12 +157,12 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
self.rescheduleApiCallsIfNecessary()
}
}
func rescheduleImmediately() {
self._attemptLoops = -1
self.rescheduleApiCallsIfNecessary()
}
/// Reschedule API calls if necessary
func rescheduleApiCallsIfNecessary() {
if self.items.isNotEmpty && !self._isRescheduling {
@ -174,19 +174,19 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Reschedule the execution of API calls
fileprivate func _waitAndExecuteApiCalls() async {
// Logger.log("\(T.resourceName()) > RESCHED")
guard !self._isRescheduling, StoreCenter.main.collectionsCanSynchronize else { return }
guard self.items.isNotEmpty else { return }
self._isRescheduling = true
self._attemptLoops += 1
await self._wait()
let batches = Dictionary(grouping: self.items, by: { $0.transactionId })
for batch in batches.values {
do {
if batch.count == 1, let apiCall = batch.first, apiCall.method == .get {
@ -212,7 +212,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Wait for an exponentionnaly long time depending on the number of attemps
fileprivate func _wait() async {
#if DEBUG
let seconds = self._attemptLoops
#else
@ -227,68 +227,33 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
Logger.error(error)
}
}
// MARK: - Synchronization
/// Returns an APICall instance for the Storable [instance] and an HTTP [method]
/// The method updates existing calls or creates a new one
// fileprivate func _call(method: HTTPMethod, instance: T? = nil) async throws -> ApiCall<T>? {
//
// if let instance {
// return try await self._callForInstance(instance, method: method)
// } else {
// if self.items.contains(where: { $0.method == .get }) {
// return nil
// } else {
// return try self._createGetCall()
// }
// }
// }
// fileprivate func _callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) async throws -> ApiCall<T> {
//
// // cleanup
// let existingCalls = self.items.filter { $0.data?.id == instance.id }
// self._deleteCalls(existingCalls)
//
// // create
// let call = try self._createCall(method, instance: instance, transactionId: transactionId)
// self._prepareCall(apiCall: call)
// }
/// The method makes some clean up when necessary:
/// - When deleting, we delete other calls as they are unecessary
/// - When updating, we delete other PUT as we don't want them to be executed in random orders
func callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) throws -> ApiCall<T> {
// cleanup
let existingCalls = self.items.filter { $0.data?.stringId == instance.stringId }
if existingCalls.count > 1 {
StoreCenter.main.log(message: "There are multiple calls registered for a single item: \(T.resourceName()), id = \(instance.stringId)")
// cleanup if necessary
switch method {
case .delete: // we don't want anything else than a DELETE in the queue
let existingCalls = self.items.filter { $0.data?.stringId == instance.stringId }
self._deleteCalls(existingCalls)
case .put: // we don't want mixed PUT calls so we delete the others
let existingPuts = self.items.filter { $0.data?.stringId == instance.stringId && $0.method == .put }
self._deleteCalls(existingPuts)
default:
break
}
let currentHTTPMethod = existingCalls.first?.method
let call: ApiCall<T>
if let currentHTTPMethod {
switch (currentHTTPMethod, method) {
case (.post, .put):
call = try self._createCall(.post, instance: instance, transactionId: transactionId)
case (.post, .delete):
call = try self._createCall(.delete, instance: instance, transactionId: transactionId)
case (.put, .put):
call = try self._createCall(.put, instance: instance, transactionId: transactionId)
case (.put, .delete):
call = try self._createCall(.delete, instance: instance, transactionId: transactionId)
default:
call = try self._createCall(method, instance: instance, transactionId: transactionId)
StoreCenter.main.log(message: "case \(currentHTTPMethod) : \(method) should not happen")
}
} else {
call = try self._createCall(method, instance: instance, transactionId: transactionId)
}
self._deleteCalls(existingCalls)
let call: ApiCall<T> = try self._createCall(method, instance: instance, transactionId: transactionId)
self._prepareCall(apiCall: call)
return call
}
fileprivate func _deleteCalls(_ calls: [ApiCall<T>]) {
for call in calls {
self.deleteById(call.id)
@ -298,7 +263,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate func _createGetCall() throws -> ApiCall<T> {
return try self._createCall(.get, instance: nil)
}
/// Creates an API call for the Storable [instance] and an HTTP [method]
fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) throws -> ApiCall<T> {
if let instance {
@ -328,7 +293,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
}
func executeBatch(_ batch: OperationBatch<T>) async throws -> [T] {
var apiCalls: [ApiCall<T>] = []
let transactionId = Store.randomId()
for insert in batch.inserts {
@ -345,7 +310,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
}
return try await self._executeApiCalls(apiCalls)
}
// /// Initiates the process of sending the data with the server
//<<<<<<< HEAD
// fileprivate func _sendServerRequest<V: Decodable>(_ method: HTTPMethod, instance: T? = nil) async throws -> V? {
@ -365,13 +330,13 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
self._prepareCall(apiCall: apiCall)
return try await self._executeGetCall(apiCall)
}
/// Executes an API call
/// For POST requests, potentially copies additional data coming from the server during the insert
fileprivate func _executeGetCall<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
return try await StoreCenter.main.executeGet(apiCall: apiCall)
}
/// 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 -> [T] {

@ -951,7 +951,7 @@ public class StoreCenter {
if let logs = self._logs {
return logs
} else {
let logsCollection: StoredCollection<Log> = Store.main.registerCollection(limit: 1000)
let logsCollection: StoredCollection<Log> = Store.main.registerCollection(limit: 50)
self._logs = logsCollection
return logsCollection
}

@ -49,10 +49,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
self.addOrUpdateNoSync(contentOfs: items)
}
}
self.setAsLoaded()
} catch {
Logger.error(error)
}
self.setAsLoaded()
}
/// Updates a local item from a server instance. This method is typically used when the server makes update

@ -153,7 +153,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
self._indexes = self.items.dictionary { $0.id }
}
}
// MARK: - Basic operations
/// Adds or updates the provided instance inside the collection

@ -37,10 +37,13 @@ struct ApiCallTests {
thing.name = "woo"
let _ = try await collection.sendUpdate(thing)
await #expect(collection.items.count == 1)
await #expect(collection.items.count == 2) // one post and one put
if let apiCall = await collection.items.first {
#expect(apiCall.method == .post)
}
if let apiCall = await collection.items.last {
#expect(apiCall.method == .put)
}
let _ = try await collection.sendDeletion(thing)
await #expect(collection.items.count == 1)
@ -60,6 +63,8 @@ struct ApiCallTests {
thing.name = "woo"
let _ = try await collection.sendUpdate(thing)
let _ = try await collection.sendUpdate(thing)
let _ = try await collection.sendUpdate(thing)
await #expect(collection.items.count == 1)
if let apiCall = await collection.items.first {
#expect(apiCall.method == .put)

Loading…
Cancel
Save