Add storeParent logic to manage directories when data is added/removed + sharingStatus refactoring to handle shared/granted states

sync3
Laurent 6 months ago
parent 27bb855c8d
commit 96b6c657e3
  1. 2
      LeStorage/Codables/ApiCall.swift
  2. 1
      LeStorage/Codables/DataAccess.swift
  3. 1
      LeStorage/Codables/DataLog.swift
  4. 1
      LeStorage/Codables/FailedAPICall.swift
  5. 7
      LeStorage/Codables/GetSyncData.swift
  6. 1
      LeStorage/Codables/Log.swift
  7. 4
      LeStorage/Codables/SyncData.swift
  8. 8
      LeStorage/ModelObject.swift
  9. 2
      LeStorage/Storable.swift
  10. 52
      LeStorage/Store.swift
  11. 81
      LeStorage/StoreCenter.swift
  12. 27
      LeStorage/StoredCollection.swift
  13. 16
      LeStorage/SyncedCollection.swift
  14. 7
      LeStorage/SyncedStorable.swift

@ -30,6 +30,7 @@ public class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
public static func resourceName() -> String { return "apicalls_" + T.resourceName() }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
@ -112,6 +113,7 @@ class OldApiCall<T: Storable>: ModelObject, Storable, SomeCall {
static func resourceName() -> String { return "apicalls_" + T.resourceName() }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func storeParent() -> Bool { return false }
var id: String = Store.randomId()

@ -13,6 +13,7 @@ class DataAccess: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "data-access" }
static func relationships() -> [Relationship] { return [] }
static var copyServerResponse: Bool = false
static func storeParent() -> Bool { return false }
override required init() {
super.init()

@ -12,6 +12,7 @@ class DataLog: ModelObject, Storable {
static func resourceName() -> String { return "data-logs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func relationships() -> [Relationship] { return [] }
static func storeParent() -> Bool { return false }
var id: String = Store.randomId()

@ -13,6 +13,7 @@ class FailedAPICall: SyncedModelObject, SyncedStorable {
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func relationships() -> [Relationship] { return [] }
static var copyServerResponse: Bool = false
static func storeParent() -> Bool { return false }
override required init() {
self.callId = ""

@ -9,6 +9,10 @@ import Foundation
class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible {
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static var copyServerResponse: Bool = false
static func storeParent() -> Bool { return false }
var date: String = ""
enum CodingKeys: String, CodingKey {
@ -25,9 +29,6 @@ class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible {
try super.init(from: decoder)
}
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static var copyServerResponse: Bool = false
static func resourceName() -> String {
return "sync-data"
}

@ -13,6 +13,7 @@ class Log: SyncedModelObject, SyncedStorable {
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func relationships() -> [Relationship] { return [] }
static var copyServerResponse: Bool = false
static func storeParent() -> Bool { return false }
override required init() {
super.init()

@ -25,6 +25,7 @@ class SyncData {
var updates: [SyncedStorableArray] = []
var deletions: [ObjectIdentifierArray] = []
var shared: [SyncedStorableArray] = []
var grants: [SyncedStorableArray] = []
var revocations: [ObjectIdentifierArray] = []
var revocationParents: [[ObjectIdentifierArray]] = []
@ -47,6 +48,9 @@ class SyncData {
if let deletions = json["deletions"] as? [String: Any] {
self.deletions = try storeCenter.decodeObjectIdentifierDictionary(deletions)
}
if let shared = json["shared"] as? [String: Any] {
self.shared = try storeCenter.decodeDictionary(shared)
}
if let grants = json["grants"] as? [String: Any] {
self.grants = try storeCenter.decodeDictionary(grants)
}

@ -56,7 +56,7 @@ open class SyncedModelObject: BaseModelObject {
public var relatedUser: String? = nil
public var lastUpdate: Date = Date()
public var shared: Bool?
public var sharing: SharingStatus?
public override init() {
super.init()
@ -73,7 +73,7 @@ open class SyncedModelObject: BaseModelObject {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.relatedUser = try container.decodeIfPresent(String.self, forKey: .relatedUser)
self.lastUpdate = try container.decodeIfPresent(Date.self, forKey: .lastUpdate) ?? Date()
self.shared = try container.decodeIfPresent(Bool.self, forKey: .shared)
self.sharing = try container.decodeIfPresent(SharingStatus.self, forKey: .shared)
try super.init(from: decoder)
}
@ -83,8 +83,8 @@ open class SyncedModelObject: BaseModelObject {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(relatedUser, forKey: .relatedUser)
try container.encode(lastUpdate, forKey: .lastUpdate)
if self.shared == true {
try container.encodeIfPresent(shared, forKey: .shared)
if self.sharing != nil {
try container.encodeIfPresent(sharing, forKey: .shared)
}
try super.encode(to: encoder)

@ -35,6 +35,8 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
/// This method returns RelationShips objects of the type
static func relationships() -> [Relationship]
static func storeParent() -> Bool
}
extension Storable {

@ -112,13 +112,13 @@ final public class Store {
/// - Parameters:
/// - indexed: Creates an index to quickly access the data
/// - inMemory: Indicates if the collection should only live in memory, and not write into a file
public func registerSynchronizedCollection<T : SyncedStorable>(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) -> SyncedCollection<T> {
public func registerSynchronizedCollection<T : SyncedStorable>(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false) -> SyncedCollection<T> {
if let collection: SyncedCollection<T> = try? self.syncedCollection() {
return collection
}
let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading)
let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading, noLoad: noLoad)
self._collections[T.resourceName()] = collection
self._baseCollections[T.resourceName()] = collection.collection
@ -207,7 +207,7 @@ final public class Store {
do {
return try self.syncedCollection()
} catch {
return self.registerSynchronizedCollection(indexed: true, inMemory: false)
return self.registerSynchronizedCollection(indexed: true, inMemory: false, noLoad: true)
}
}
@ -253,7 +253,7 @@ final public class Store {
/// Calls addOrUpdateIfNewer from the collection corresponding to the instance
@MainActor
func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T, shared: Bool) {
func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T, shared: SharingStatus?) {
let collection: SyncedCollection<T> = self.registerOrGetSyncedCollection(T.self)
collection.addOrUpdateIfNewer(instance, shared: shared)
}
@ -294,7 +294,7 @@ final public class Store {
}
}
public func deleteUnusedSharedDependencies<T: SyncedStorable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
public func deleteUnusedSharedDependencies<T: SyncedStorable>(type: T.Type, _ handler: (T) throws -> Bool) {
do {
let collection: SyncedCollection<T> = try self.syncedCollection()
let items = try collection.items.filter(handler)
@ -305,12 +305,21 @@ final public class Store {
}
public func deleteUnusedSharedDependencies<T: SyncedStorable>(type: T.Type) {
do {
let collection: SyncedCollection<T> = try self.syncedCollection()
self.deleteUnusedSharedDependencies(collection.items)
} catch {
Logger.error(error)
}
}
/// Deletes dependencies of shared objects that are not used elsewhere in the system
/// Similar to _deleteDependencies but only for unused shared objects
public func deleteUnusedSharedDependencies<T: SyncedStorable>(_ items: [T]) {
do {
for item in items {
guard item.shared == true else { continue }
guard item.sharing != nil else { continue }
if self.referenceCount(type: T.self, id: item.stringId) == 0 {
// Only delete if the shared item has no references
item.deleteUnusedSharedDependencies(store: self)
@ -333,17 +342,40 @@ final public class Store {
}
public func deleteDependencies<T: Storable>(type: T.Type, actionOption: ActionOption, _ isIncluded: (any Storable) -> Bool) {
public func deleteDependencies<T>(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) where T: SyncedStorable {
do {
let collection: any SomeCollection = try self.someCollection(type: type)
collection.deleteDependencies(actionOption: actionOption, isIncluded)
let collection = try self.someCollection(type: type)
if let syncCollection = collection as? SyncedCollection<T> {
syncCollection.deleteDependencies(actionOption: actionOption, isIncluded)
}
} catch {
Logger.error(error)
}
}
public func deleteDependencies<T>(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) where T: Storable {
do {
let collection = try self.someCollection(type: type)
if let syncCollection = collection as? StoredCollection<T> {
syncCollection.deleteDependencies(actionOption: actionOption, isIncluded)
}
} catch {
Logger.error(error)
}
}
// public func deleteDependencies<T: Storable>(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) {
//
// do {
// let collection: any SomeCollection = try self.someCollection(type: type)
// if let syncCollection = collection as? SyncedCollection<T> {
// collection.deleteDependencies(actionOption: actionOption, isIncluded)
// } catch {
// Logger.error(error)
// }
//
// }
// MARK: - Write
/// Returns the directory URL of the store

@ -183,6 +183,8 @@ public class StoreCenter {
}
}
// MARK: - Store management
/// Registers a store into the list of stores
/// - Parameters:
/// - store: A store to save
@ -200,7 +202,7 @@ public class StoreCenter {
/// - Parameters:
/// - identifier: The store identifer
/// - parameter: The parameter name used to filter data on the server
public func requestStore(identifier: String) -> Store {
func requestStore(identifier: String) -> Store {
if let store = self._stores[identifier] {
return store
} else {
@ -210,6 +212,29 @@ public class StoreCenter {
}
}
/// Returns the store corresponding to the provided id, and creates one if necessary, otherwise returns the main store
fileprivate func _requestStore(id: String?) -> Store {
if let storeId = id {
if let store = self._stores[storeId] {
return store
} else {
let store = Store(storeCenter: self, identifier: storeId)
self._registerStore(store: store)
return store
}
} else {
return self.mainStore
}
}
fileprivate func _store(id: String?) -> Store? {
if let storeId = id, let store = self._stores[storeId] {
return store
} else {
return self.mainStore
}
}
public func store(identifier: String) throws -> Store {
if let store = self._stores[identifier] {
return store
@ -217,6 +242,15 @@ public class StoreCenter {
throw StoreError.storeNotRegistered(id: identifier)
}
/// Deletes the directory using its identifier
/// - Parameters:
/// - identifier: The name of the directory
public func destroyStore(identifier: String) {
let directory = "\(self.directoryName)/\(identifier)"
FileManager.default.deleteDirectoryInDocuments(directoryName: directory)
self._stores.removeValue(forKey: identifier)
}
// MARK: - Settings
/// Sets the user info given a user
@ -535,7 +569,7 @@ public class StoreCenter {
}
func itemsRetrieved<T: SyncedStorable>(_ results: [T], storeId: String?, clear: Bool) async {
await self._store(id: storeId).loadCollectionItems(results, clear: clear)
await self._requestStore(id: storeId).loadCollectionItems(results, clear: clear)
}
/// Returns the names of all collections
@ -631,7 +665,7 @@ public class StoreCenter {
}
let array = try self.decodeDictionary(json)
await self._syncAddOrUpdate(array, shared: true)
await self._syncAddOrUpdate(array, shared: .shared)
} catch {
Logger.error(error)
}
@ -642,11 +676,12 @@ public class StoreCenter {
await self._syncAddOrUpdate(syncData.updates)
await self._syncDelete(syncData.deletions)
await self._syncAddOrUpdate(syncData.grants, shared: true)
await self._syncAddOrUpdate(syncData.shared, shared: .shared)
await self._syncAddOrUpdate(syncData.grants, shared: .granted)
await self.syncRevoke(syncData.revocations, parents: syncData.revocationParents)
// self._syncAddOrUpdate(syncData.relationshipSets)
// await self._syncDelete(syncData.relationshipRemovals)
await self._syncAddOrUpdate(syncData.sharedRelationshipSets, shared: true)
await self._syncAddOrUpdate(syncData.sharedRelationshipSets, shared: .granted)
await self._syncRevoke(syncData.sharedRelationshipRemovals)
Logger.log("sync content: updates = \(syncData.updates.count) / deletions = \(syncData.deletions.count), grants = \(syncData.grants.count)")
@ -668,7 +703,7 @@ public class StoreCenter {
/// - updateArrays: the server updates
/// - shared: indicates if the content should be flagged as shared
@MainActor
func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: Bool = false) async {
func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: SharingStatus? = nil) async {
for updateArray in updateArrays {
for item in updateArray.items {
@ -727,21 +762,6 @@ public class StoreCenter {
}
/// Returns the store corresponding to the provided id, and creates one if necessary
fileprivate func _store(id: String?) -> Store {
if let storeId = id {
if let store = self._stores[storeId] {
return store
} else {
let store = Store(storeCenter: self, identifier: storeId)
self._registerStore(store: store)
return store
}
} else {
return self.mainStore
}
}
/// Returns whether a data has already been deleted by, to avoid inserting it again
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool {
return self._deleteLogs.contains(where: {
@ -750,10 +770,10 @@ public class StoreCenter {
}
/// Adds or updates an instance into the store
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?, shared: Bool) async {
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?, shared: SharingStatus?) async {
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance)
if !hasAlreadyBeenDeleted {
await self._store(id: storeId).addOrUpdateIfNewer(instance, shared: shared)
await self._requestStore(id: storeId).addOrUpdateIfNewer(instance, shared: shared)
}
}
@ -761,7 +781,7 @@ public class StoreCenter {
@MainActor
func synchronizationDelete<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) {
do {
try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id)
try self._store(id: storeId)?.deleteNoSyncNoCascade(type: type, id: id)
} catch {
Logger.error(error)
}
@ -776,7 +796,7 @@ public class StoreCenter {
if self._instanceShared(id: id, type: type) {
let count = self.mainStore.referenceCount(type: type, id: id)
if count == 0 {
try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id)
try self._store(id: storeId)?.deleteNoSyncNoCascade(type: type, id: id)
}
}
} catch {
@ -788,7 +808,7 @@ public class StoreCenter {
fileprivate func _instanceShared<T: SyncedStorable>(id: String, type: T.Type) -> Bool {
let realId: T.ID = T.buildRealId(id: id)
let instance: T? = self.mainStore.findById(realId)
return instance?.shared == true
return instance?.sharing != nil
}
/// Deletes a data log by data id
@ -967,15 +987,6 @@ public class StoreCenter {
return !self._blackListedUserName.contains(where: { $0 == userName })
}
/// Deletes the directory using its identifier
/// - Parameters:
/// - identifier: The name of the directory
public func destroyStore(identifier: String) {
let directory = "\(self.directoryName)/\(identifier)"
FileManager.default.deleteDirectoryInDocuments(directoryName: directory)
self._stores.removeValue(forKey: identifier)
}
// MARK: - Instant update
/// Updates a local object with a server instance

@ -105,7 +105,7 @@ public class StoredCollection<T: Storable>: SomeCollection {
}
}
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, delegate: (any CollectionDelegate<T>)? = nil) {
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false, delegate: (any CollectionDelegate<T>)? = nil) {
if indexed {
self._indexes = [:]
}
@ -114,15 +114,18 @@ public class StoredCollection<T: Storable>: SomeCollection {
self.limit = limit
self._delegate = delegate
if synchronousLoading {
Task {
await self.loadFromFile()
}
if noLoad {
self.hasLoaded = true
} else {
Task(priority: .high) {
await self.load()
Task {
if synchronousLoading {
await self.loadFromFile()
} else {
await self.load()
}
}
}
}
init(store: Store) {
@ -322,6 +325,11 @@ public class StoredCollection<T: Storable>: SomeCollection {
instance.store = self.store
self._indexes?[instance.id] = instance
self._applyLimitIfPresent()
if T.storeParent() {
_ = self.storeCenter.requestStore(identifier: instance.stringId) // make directory
}
return true
}
@ -361,6 +369,11 @@ public class StoredCollection<T: Storable>: SomeCollection {
}
self.localDeleteOnly(instance: instance)
if T.storeParent() {
self.storeCenter.destroyStore(identifier: instance.stringId)
}
return true
}

@ -19,10 +19,10 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
let store: Store
let collection: StoredCollection<T>
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) {
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false) {
self.store = store
self.collection = StoredCollection<T>(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading)
self.collection = StoredCollection<T>(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading, noLoad: noLoad)
}
@ -115,7 +115,7 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
let result = self.collection.addOrUpdate(instance: instance)
if result.method == .update {
if instance.shared == true {
if instance.sharing != nil {
self._cleanUpSharedDependencies()
}
}
@ -252,7 +252,7 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
fileprivate func _deleteUnusedSharedInstances() {
let sharedItems = self.collection.items.filter { $0.shared == true }
let sharedItems = self.collection.items.filter { $0.sharing != nil }
for sharedItem in sharedItems {
self.store.deleteUnusedSharedIfNecessary(sharedItem)
}
@ -331,7 +331,7 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
func deleteUnusedShared(instance: T) {
guard instance.shared == true else { return }
guard instance.sharing != nil else { return }
self.delete(instance: instance)
instance.deleteUnusedSharedDependencies(store: self.store)
@ -412,7 +412,7 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
// MARK: - Synchronization
/// Adds or update an instance if it is newer than the local instance
func addOrUpdateIfNewer(_ instance: T, shared: Bool) {
func addOrUpdateIfNewer(_ instance: T, shared: SharingStatus?) {
// defer {
// self.triggerWrite()
@ -426,9 +426,7 @@ public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, Collect
print("do not update \(T.resourceName()): \(instance.lastUpdate.timeIntervalSince1970) / local: \(localInstance.lastUpdate.timeIntervalSince1970)")
}
} else { // insert
if shared {
instance.shared = true
}
instance.sharing = shared
self.collection.add(instance: instance, actionOption: .standard)
}

@ -7,10 +7,15 @@
import Foundation
public enum SharingStatus: Int, Codable {
case shared = 1
case granted
}
public protocol SyncedStorable: Storable {
var lastUpdate: Date { get set }
var shared: Bool? { get set }
var sharing: SharingStatus? { get set }
init()

Loading…
Cancel
Save