sync_v2
Laurent 7 months ago
parent c5f5c67737
commit f32bc866f5
  1. 86
      LeStorage/BaseCollection.swift
  2. 8
      LeStorage/Codables/PendingOperation.swift
  3. 2
      LeStorage/ModelObject.swift
  4. 4
      LeStorage/PendingOperationManager.swift
  5. 4
      LeStorage/Services.swift
  6. 2
      LeStorage/Storable.swift
  7. 51
      LeStorage/StoreCenter.swift
  8. 40
      LeStorage/SyncedCollection.swift
  9. 2
      LeStorage/WebSocketManager.swift

@ -41,7 +41,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
fileprivate var _indexes: [T.ID: T]? = nil
/// A PendingOperationManager instance that manages operations while the collection is not loaded
fileprivate var _pendingOperationManager: PendingOperationManager<T>? = nil
fileprivate(set) var pendingOperationManager: PendingOperationManager<T>? = nil
/// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _hasChanged: Bool = false {
@ -145,24 +145,6 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
name: NSNotification.Name.CollectionDidLoad, object: self)
}
fileprivate func _mergePendingOperations() {
guard let manager = self._pendingOperationManager, manager.items.isNotEmpty else { return }
Logger.log(">>> Merge pending: \(manager.items.count)")
let methodGroups = manager.items.group { $0.method }
for (method, group) in methodGroups {
let dataArray = group.map { $0.data }
switch method {
case .addOrUpdate:
self.addOrUpdate(contentOfs: dataArray)
case .delete:
self.delete(contentOfs: dataArray)
}
}
self._pendingOperationManager = nil
}
/// Sets a collection of items and indexes them
func setItems(_ items: [T]) {
for item in items {
@ -272,10 +254,10 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
/// Adds an instance to the collection
@discardableResult func addItem(instance: T, checkLoaded: Bool = true) -> Bool {
@discardableResult func addItem(instance: T, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool {
if checkLoaded && !self.hasLoaded {
self._addPendingOperation(method: .addOrUpdate, instance: instance)
self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
return false
}
@ -288,10 +270,10 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
/// Updates an instance to the collection by index
@discardableResult func updateItem(_ instance: T, index: Int, checkLoaded: Bool = true) -> Bool {
@discardableResult func updateItem(_ instance: T, index: Int, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool {
if checkLoaded && !self.hasLoaded {
self._addPendingOperation(method: .addOrUpdate, instance: instance)
self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
return false
}
@ -306,26 +288,19 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
/// Deletes an instance from the collection
@discardableResult func deleteItem(_ instance: T) -> Bool {
@discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool {
if !self.hasLoaded {
self._addPendingOperation(method: .addOrUpdate, instance: instance)
self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
return false
}
instance.deleteDependencies()
instance.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
self.items.removeAll { $0.id == instance.id }
self._indexes?.removeValue(forKey: instance.id)
return true
}
fileprivate func _addPendingOperation(method: StorageMethod, instance: T) {
if self._pendingOperationManager == nil {
self._pendingOperationManager = PendingOperationManager<T>(store: self.store, inMemory: self.inMemory)
}
self._pendingOperationManager?.addPendingOperation(method: method, instance: instance)
}
/// If the collection has more instance that its limit, remove the surplus
fileprivate func _applyLimitIfPresent() {
if let limit {
@ -361,6 +336,51 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
self.delete(contentOfs: self.items)
}
// MARK: - Pending operations
fileprivate func _addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) {
if self.pendingOperationManager == nil {
self.pendingOperationManager = PendingOperationManager<T>(store: self.store, inMemory: self.inMemory)
}
self.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: false)
}
func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) {
self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
}
fileprivate func _mergePendingOperations() {
guard let manager = self.pendingOperationManager, manager.items.isNotEmpty else { return }
Logger.log(">>> Merge pending: \(manager.items.count)")
for item in manager.items {
let data = item.data
switch (item.method, item.shouldBeSynchronized) {
case (.addOrUpdate, true):
self.addOrUpdate(instance: data)
case (.addOrUpdate, false):
self.addOrUpdateItem(instance: data)
case (.delete, true):
self.delete(instance: data)
case (.delete, false):
self.deleteItem(data)
}
}
// let methodGroups = manager.items.group { $0.method }
// for (method, group) in methodGroups {
// let dataArray = group.map { $0.data }
// switch method {
// case .addOrUpdate:
// self.addOrUpdate(contentOfs: dataArray)
// case .delete:
// self.delete(contentOfs: dataArray)
// }
// }
self.pendingOperationManager = nil
}
// MARK: - File access
/// Schedules a write operation

@ -17,14 +17,12 @@ class PendingOperation<T : Storable>: Codable, Equatable {
var id: String = Store.randomId()
var method: StorageMethod
var data: T
// var modelName: String
// var storeId: String?
var shouldBeSynchronized: Bool
init(method: StorageMethod, data: T) {
// self.modelName = modelName
// self.storeId = storeId
init(method: StorageMethod, data: T, shouldBeSynchronized: Bool) {
self.method = method
self.data = data
self.shouldBeSynchronized = shouldBeSynchronized
}
static func == (lhs: PendingOperation, rhs: PendingOperation) -> Bool {

@ -15,7 +15,7 @@ open class ModelObject: NSObject {
public override init() { }
open func deleteDependencies() {
open func deleteDependencies(shouldBeSynchronized: Bool) {
}

@ -32,10 +32,10 @@ class PendingOperationManager<T: Storable> {
}
}
func addPendingOperation(method: StorageMethod, instance: T) {
func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) {
Logger.log("addPendingOperation: \(method), \(instance)")
let operation = PendingOperation<T>(method: method, data: instance)
let operation = PendingOperation<T>(method: method, data: instance, shouldBeSynchronized: shouldBeSynchronized)
self.items.append(operation)
self._writeIfNecessary()

@ -34,7 +34,7 @@ let changePasswordCall: ServiceCall = ServiceCall(
let postDeviceTokenCall: ServiceCall = ServiceCall(
path: "device-token/", method: .post, requiresToken: true)
let getUserDataAccessCall: ServiceCall = ServiceCall(
path: "data-access/", method: .get, requiresToken: true)
path: "data-access-content/", method: .get, requiresToken: true)
let userNamesCall: ServiceCall = ServiceCall(
path: "user-names/", method: .get, requiresToken: true)
@ -632,7 +632,7 @@ public class Services {
}
/// Returns the list of DataAccess
public func getUserDataAccess() async throws {
func getUserDataAccess() async throws {
let request = try self._baseRequest(call: getUserDataAccessCall)
if let data = try await self._runRequest(request) {
await StoreCenter.main.userDataAccessRetrieved(data)

@ -21,7 +21,7 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
/// 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()
func deleteDependencies(shouldBeSynchronized: Bool)
/// 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

@ -577,6 +577,23 @@ public class StoreCenter {
if let revocations = json["revocations"] as? [String: Any] {
try self._parseSyncRevocations(revocations, parents: json["revocation_parents"] as? [[String: Any]])
}
// Data access events
if let rs = json["relationship_sets"] as? [String: Any] {
try self._parseSyncUpdates(rs)
}
if let rr = json["relationship_removals"] as? [String: Any] {
try self._parseSyncDeletions(rr)
}
if let srs = json["shared_relationship_sets"] as? [String: Any] {
try self._parseSyncUpdates(srs, shared: true)
}
if let srm = json["shared_relationship_removals"] as? [String: Any] {
self._synchronizationRevoke(items: srm)
}
if let dateString = json["date"] as? String {
Logger.log("Sets sync date = \(dateString)")
@ -671,23 +688,28 @@ public class StoreCenter {
if let parents {
for level in parents {
for (className, parentData) in level {
guard let parentItems = parentData as? [Any] else {
Logger.w("Invalid update data for \(className): \(parentData)")
continue
}
for parentItem in parentItems {
do {
let data = try JSONSerialization.data(withJSONObject: parentItem, options: [])
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data)
StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId)
} catch {
Logger.error(error)
}
}
self._synchronizationRevoke(items: level)
}
}
}
fileprivate func _synchronizationRevoke(items: [String: Any]) {
for (className, parentData) in items {
guard let parentItems = parentData as? [Any] else {
Logger.w("Invalid update data for \(className): \(parentData)")
continue
}
for parentItem in parentItems {
do {
let data = try JSONSerialization.data(withJSONObject: parentItem, options: [])
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data)
StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId)
} catch {
Logger.error(error)
}
}
}
}
/// Returns a Type object for a class name
@ -987,6 +1009,7 @@ public class StoreCenter {
} else {
dataAccess.sharedWith.removeAll()
dataAccess.sharedWith = users
dataAccessCollection.addOrUpdate(instance: dataAccess)
}
} else {
let dataAccess = DataAccess(owner: userId, sharedWith: users, modelName: String(describing: type(of: instance)), modelId: instance.stringId)

@ -92,12 +92,12 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
instance.lastUpdate = Date()
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
self._sendUpdate(instance)
self.setChanged()
}
} else {
if self.addItem(instance: instance) {
if self.addItem(instance: instance, shouldBeSynchronized: true) {
self._sendInsertion(instance)
self.setChanged()
}
@ -117,11 +117,11 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
for instance in sequence {
instance.lastUpdate = date
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
batch.addUpdate(instance)
}
} else { // insert
if self.addItem(instance: instance) {
if self.addItem(instance: instance, shouldBeSynchronized: true) {
batch.addInsert(instance)
}
}
@ -137,7 +137,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
override public func delete(contentOfs sequence: any RandomAccessCollection<T>) {
public override func delete(contentOfs sequence: any RandomAccessCollection<T>) {
defer {
self.setChanged()
@ -148,7 +148,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
var deleted: [T] = []
for instance in sequence {
if self.deleteItem(instance) {
if self.deleteItem(instance, shouldBeSynchronized: true) {
deleted.append(instance)
}
StoreCenter.main.createDeleteLog(instance)
@ -169,15 +169,19 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Deletes an instance without writing, logs the operation and sends an API call
fileprivate func _deleteNoWrite(instance: T) {
self.deleteItem(instance)
self.deleteItem(instance, shouldBeSynchronized: true)
StoreCenter.main.createDeleteLog(instance)
self._sendDeletion(instance)
}
public func deleteDependencies(_ items: any RandomAccessCollection<T>) {
public func deleteDependencies(_ items: any RandomAccessCollection<T>, shouldBeSynchronized: Bool) {
guard items.isNotEmpty else { return }
self.delete(contentOfs: items)
if shouldBeSynchronized {
self.delete(contentOfs: items)
} else {
self.deleteNoSync(contentOfs: items)
}
}
// MARK: - Basic operations without sync
@ -193,11 +197,21 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
/// Deletes the instance in the collection without synchronization
func deleteNoSync(instance: T) throws {
func deleteNoSync(contentOfs sequence: any Sequence<T>) {
defer {
self.setChanged()
}
for item in sequence {
self.deleteItem(item, shouldBeSynchronized: false)
}
}
/// Deletes the instance in the collection without synchronization
func deleteNoSync(instance: T) {
defer {
self.setChanged()
}
self.deleteItem(instance)
self.deleteItem(instance, shouldBeSynchronized: false)
}
/// Deletes the instance in the collection without synchronization
@ -207,7 +221,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
let realId = T.buildRealId(id: id)
if let instance = self.findById(realId) {
self.deleteItem(instance)
self.deleteItem(instance, shouldBeSynchronized: false)
}
}
@ -311,7 +325,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
if shared {
instance.shared = true
}
self.addItem(instance: instance)
self.addItem(instance: instance, shouldBeSynchronized: false)
}
}

@ -21,7 +21,7 @@ class WebSocketManager: ObservableObject {
init(urlString: String) {
self._url = urlString
// _setupWebSocket()
_setupWebSocket()
}
deinit {

Loading…
Cancel
Save