Refactor SyncedCollection to own StoredCollection instead of inheriting it

sync3
Laurent 6 months ago
parent 28687133f6
commit 9efc8b14c8
  1. 12
      LeStorage.xcodeproj/project.pbxproj
  2. 1
      LeStorage/ApiCallCollection.swift
  3. 9
      LeStorage/Codables/PendingOperation.swift
  4. 2
      LeStorage/ModelObject.swift
  5. 4
      LeStorage/PendingOperationManager.swift
  6. 2
      LeStorage/Storable.swift
  7. 79
      LeStorage/Store.swift
  8. 36
      LeStorage/StoreCenter.swift
  9. 258
      LeStorage/StoredCollection.swift
  10. 4
      LeStorage/StoredSingleton.swift
  11. 378
      LeStorage/SyncedCollection.swift
  12. 18
      LeStorage/SyncedStorable.swift
  13. 17
      LeStorage/Utils/String+Extensions.swift
  14. 3
      LeStorageTests/IdentifiableTests.swift
  15. 2
      LeStorageTests/StoredCollectionTests.swift

@ -21,9 +21,10 @@
C48638B32D9BC6A8007E3E06 /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */; };
C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C87F2CCBDC210082001F /* NetworkMonitor.swift */; };
C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49774DE2DC4B3D7005CD239 /* SyncData.swift */; };
C49779FC2DDB5D89005CD239 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49779FB2DDB5D89005CD239 /* String+Extensions.swift */; };
C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; };
C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; };
C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */; };
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */; };
C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */; };
C4A47D532B6D2C5F00ADC637 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D522B6D2C5F00ADC637 /* Logger.swift */; };
C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */; };
@ -79,9 +80,10 @@
C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = "<group>"; };
C488C87F2CCBDC210082001F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
C49774DE2DC4B3D7005CD239 /* SyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncData.swift; sourceTree = "<group>"; };
C49779FB2DDB5D89005CD239 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = "<group>"; };
C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = "<group>"; };
C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollection.swift; sourceTree = "<group>"; };
C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCollection.swift; sourceTree = "<group>"; };
C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = "<group>"; };
C4A47D522B6D2C5F00ADC637 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
@ -169,7 +171,7 @@
C425D4572B6D2519002A7B48 /* Store.swift */,
C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */,
C4A47D642B6E92FE00ADC637 /* Storable.swift */,
C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */,
C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */,
C4D477A02CB9586A0077713D /* SyncedCollection.swift */,
C456EFE12BE52379007388E2 /* StoredSingleton.swift */,
C4D4779E2CB92FD80077713D /* SyncedStorable.swift */,
@ -198,6 +200,7 @@
C4A47D522B6D2C5F00ADC637 /* Logger.swift */,
C4B96E1C2D8C53D700C2955F /* UIDevice+Extensions.swift */,
C4FAE69B2CEB8E9500790446 /* URLManager.swift */,
C49779FB2DDB5D89005CD239 /* String+Extensions.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -363,8 +366,9 @@
C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */,
C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */,
C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */,
C49779FC2DDB5D89005CD239 /* String+Extensions.swift in Sources */,
C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */,
C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */,
C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */,
C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */,

@ -442,6 +442,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Returns if the API call collection is not empty
func hasPendingCalls() -> Bool {
// print("\(T.resourceName()) calls = \(self.items.count)")
return self.items.isNotEmpty
}

@ -8,7 +8,8 @@
import Foundation
enum StorageMethod: String, Codable {
case addOrUpdate
case add
case update
case delete
case deleteUnusedShared
}
@ -18,12 +19,12 @@ class PendingOperation<T : Storable>: Codable, Equatable {
var id: String = Store.randomId()
var method: StorageMethod
var data: T
var shouldBeSynchronized: Bool
var actionOption: ActionOption
init(method: StorageMethod, data: T, shouldBeSynchronized: Bool) {
init(method: StorageMethod, data: T, actionOption: ActionOption) {
self.method = method
self.data = data
self.shouldBeSynchronized = shouldBeSynchronized
self.actionOption = actionOption
}
static func == (lhs: PendingOperation, rhs: PendingOperation) -> Bool {

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

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

@ -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(store: Store, shouldBeSynchronized: Bool)
func deleteDependencies(store: Store, actionOption: ActionOption)
/// A method that deletes dependencies of shared resources, but only if they are themselves shared
/// and not referenced by other objects in the store

@ -18,6 +18,7 @@ public enum StoreError: Error, LocalizedError {
case apiCallCollectionNotRegistered(type: String)
case synchronizationInactive
case storeNotRegistered(id: String)
case castIssue(type: String)
public var errorDescription: String? {
switch self {
@ -39,6 +40,8 @@ public enum StoreError: Error, LocalizedError {
return "The synchronization is not active on this StoreCenter"
case .storeNotRegistered(let id):
return "The store with identifier \(id) is not registered"
case .castIssue(let type):
return "Can't cast to \(type)"
}
}
@ -51,6 +54,9 @@ final public class Store {
/// The dictionary of registered collections
fileprivate var _collections: [String : any SomeCollection] = [:]
/// The dictionary of all StoredCollection
fileprivate var _baseCollections: [String : any SomeCollection] = [:]
/// The store identifier, used to name the store directory, and to perform filtering requests to the server
public fileprivate(set) var identifier: String? = nil
@ -90,12 +96,14 @@ final public class Store {
/// - inMemory: Indicates if the collection should only live in memory, and not write into a file
public func registerCollection<T : Storable>(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil) -> StoredCollection<T> {
if let collection: StoredCollection<T> = try? self.collection() as? StoredCollection<T> {
return collection
if let _ = try? self.someCollection(type: T.self) {
fatalError("collection already registered")
// return collection
}
let collection = StoredCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit)
self._collections[T.resourceName()] = collection
self._baseCollections[T.resourceName()] = collection
return collection
}
@ -112,6 +120,8 @@ final public class Store {
let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading)
self._collections[T.resourceName()] = collection
self._baseCollections[T.resourceName()] = collection.collection
self.storeCenter.loadApiCallCollection(type: T.self)
return collection
}
@ -119,6 +129,8 @@ final public class Store {
func asyncLoadingSynchronizedCollection<T : SyncedStorable>(inMemory: Bool = false) async -> SyncedCollection<T> {
let collection = await SyncedCollection<T>(store: self, inMemory: inMemory)
self._collections[T.resourceName()] = collection
self._baseCollections[T.resourceName()] = collection.collection
self.storeCenter.loadApiCallCollection(type: T.self)
return collection
}
@ -126,6 +138,8 @@ final public class Store {
func asyncLoadingStoredCollection<T : Storable>(inMemory: Bool = false) async -> StoredCollection<T> {
let collection = await StoredCollection<T>(store: self, inMemory: inMemory)
self._collections[T.resourceName()] = collection
self._baseCollections[T.resourceName()] = collection
return collection
}
@ -153,7 +167,7 @@ final public class Store {
/// - Parameters:
/// - id: the id of the data
public func findById<T: Storable>(_ id: T.ID) -> T? {
guard let collection = self._collections[T.resourceName()] as? BaseCollection<T> else {
guard let collection = self._baseCollections[T.resourceName()] as? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered")
return nil
}
@ -174,13 +188,21 @@ final public class Store {
}
/// Returns a collection by type
func collection<T: Storable>() throws -> BaseCollection<T> {
if let collection = self._collections[T.resourceName()] as? BaseCollection<T> {
func someCollection<T: Storable>(type: T.Type) throws -> any SomeCollection {
if let collection = self._collections[T.resourceName()] {
return collection
}
throw StoreError.collectionNotRegistered(type: T.resourceName())
}
/// Returns a collection by type
// func collection<T: Storable>() throws -> BaseCollection<T> {
// if let collection = self._collections[T.resourceName()] as? BaseCollection<T> {
// return collection
// }
// throw StoreError.collectionNotRegistered(type: T.resourceName())
// }
func registerOrGetSyncedCollection<T: SyncedStorable>(_ type: T.Type) -> SyncedCollection<T> {
do {
return try self.syncedCollection()
@ -236,19 +258,19 @@ final public class Store {
}
/// Calls deleteById from the collection corresponding to the instance
func deleteNoSync<T: Storable>(instance: T) {
do {
let collection: BaseCollection<T> = try self.collection()
collection.delete(instance: instance)
} catch {
Logger.error(error)
}
}
// func deleteNoSync<T: Storable>(instance: T) {
// do {
// let collection: BaseCollection<T> = try self.collection()
// collection.delete(instance: instance)
// } catch {
// Logger.error(error)
// }
// }
/// Calls deleteById from the collection corresponding to the instance
func deleteNoSync<T: SyncedStorable>(type: T.Type, id: String) throws {
func deleteNoSyncNoCascade<T: SyncedStorable>(type: T.Type, id: String) throws {
let collection: SyncedCollection<T> = try self.syncedCollection()
collection.deleteByStringIdNoSync(id)
collection.deleteNoSyncNoCascade(id: id)
}
/// Calls deleteById from the collection corresponding to the instance
@ -273,7 +295,7 @@ final public class Store {
public func deleteUnusedSharedDependencies<T: SyncedStorable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
let collection: SyncedCollection<T> = try self.syncedCollection()
let items = try collection.items.filter(handler)
self.deleteUnusedSharedDependencies(items)
} catch {
@ -300,36 +322,25 @@ final public class Store {
}
}
public func deleteAllDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool) {
public func deleteAllDependencies<T: Storable>(type: T.Type, actionOption: ActionOption) {
do {
let collection: BaseCollection<T> = try self.collection()
try self._deleteDependencies(Array(collection.items), shouldBeSynchronized: shouldBeSynchronized)
let collection = try self.someCollection(type: type)
collection.deleteAllItemsAndDependencies(actionOption: actionOption)
} catch {
Logger.error(error)
}
}
public func deleteDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
let items = try collection.items.filter(handler)
try self._deleteDependencies(items, shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
}
public func deleteDependencies<T: Storable>(type: T.Type, actionOption: ActionOption, _ isIncluded: (any Storable) -> Bool) {
fileprivate func _deleteDependencies<T: Storable>(_ items: [T], shouldBeSynchronized: Bool) throws {
do {
let collection: BaseCollection<T> = try self.collection()
for item in items {
item.deleteDependencies(store: self, shouldBeSynchronized: shouldBeSynchronized)
}
collection.deleteDependencies(items)
let collection: any SomeCollection = try self.someCollection(type: type)
collection.deleteDependencies(actionOption: actionOption, isIncluded)
} catch {
Logger.error(error)
}
}
// MARK: - Write

@ -765,7 +765,7 @@ public class StoreCenter {
@MainActor
func synchronizationDelete<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) {
do {
try self._store(id: storeId).deleteNoSync(type: type, id: id)
try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id)
} catch {
Logger.error(error)
}
@ -780,7 +780,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).deleteNoSync(type: type, id: id)
try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id)
}
}
} catch {
@ -994,29 +994,31 @@ public class StoreCenter {
}
/// Returns the collection hosting an instance
func collectionOfInstance<T: Storable>(_ instance: T) -> BaseCollection<T>? {
func collectionOfInstance<T: Storable>(_ instance: T) -> (any SomeCollection)? {
do {
let collection: BaseCollection<T> = try self.mainStore.collection()
if collection.findById(instance.id) != nil {
return collection
if let storeId = instance.getStoreId() {
let store = try self.store(identifier: storeId)
return try store.someCollection(type: T.self)
} else {
return self.collectionOfInstanceInSubStores(instance)
return try Store.main.someCollection(type: T.self)
}
} catch {
return self.collectionOfInstanceInSubStores(instance)
Logger.error(error)
}
return nil
}
/// Search inside the additional stores to find the collection hosting the instance
func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> BaseCollection<T>? {
for store in self._stores.values {
let collection: BaseCollection<T>? = try? store.collection()
if collection?.findById(instance.id) != nil {
return collection
}
}
return nil
}
// func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> BaseCollection<T>? {
// for store in self._stores.values {
// let collection: BaseCollection<T>? = try? store.collection()
// if collection?.findById(instance.id) != nil {
// return collection
// }
// }
// return nil
// }
// MARK: - Data Access

@ -1,5 +1,5 @@
//
// BaseCollection.swift
// StoredCollection.swift
// LeStorage
//
// Created by Laurent Morvillier on 02/02/2024.
@ -8,26 +8,57 @@
import Foundation
import Combine
public protocol CollectionHolder {
associatedtype Item: Storable
public protocol SomeCollection<Item>: Identifiable {
var items: [Item] { get }
func reset()
}
public protocol SomeCollection: CollectionHolder, Identifiable {
associatedtype Item: Storable
var resourceName: String { get }
var hasLoaded: Bool { get }
var inMemory: Bool { get }
var type: any Storable.Type { get }
func reset()
func referenceCount<S: Storable>(type: S.Type, id: String) -> Int
var items: [Item] { get }
func deleteAllItemsAndDependencies(actionOption: ActionOption)
func deleteDependencies(actionOption: ActionOption, _ isIncluded: (Item) -> Bool)
func findById(_ id: Item.ID) -> Item?
}
protocol CollectionDelegate<Item> {
associatedtype Item: Storable
func loadingForMemoryCollection() async
func itemMerged(_ pendingOperation: PendingOperation<Item>)
}
enum CollectionMethod {
case insert
case update
case delete
}
public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
public struct ActionResult<T> {
var instance: T
var method: CollectionMethod
var pending: Bool
}
public struct ActionOption: Codable {
var synchronize: Bool
var cascade: Bool
static let standard: ActionOption = ActionOption(synchronize: false, cascade: false)
static let cascade: ActionOption = ActionOption(synchronize: false, cascade: true)
static let syncedCascade: ActionOption = ActionOption(synchronize: true, cascade: true)
}
public class StoredCollection<T: Storable>: SomeCollection {
public typealias Item = T
/// Doesn't write the collection in a file
fileprivate(set) public var inMemory: Bool = false
@ -47,7 +78,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
/// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _triggerWrite: Bool = false {
didSet {
if self._triggerWrite == true {
if self._triggerWrite == true && self.inMemory == false {
self._scheduleWrite()
DispatchQueue.main.async {
@ -65,6 +96,8 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
/// Sets a max number of items inside the collection
fileprivate(set) var limit: Int? = nil
fileprivate var _delegate: (any CollectionDelegate<T>)? = nil
init(store: Store, inMemory: Bool = false) async {
self.store = store
if self.inMemory == false {
@ -72,13 +105,14 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
}
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, delegate: (any CollectionDelegate<T>)? = nil) {
if indexed {
self._indexes = [:]
}
self.inMemory = inMemory
self.store = store
self.limit = limit
self._delegate = delegate
if synchronousLoading {
Task {
@ -109,7 +143,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
// MARK: - Loading
/// Sets the collection as changed to trigger a write
func triggerWrite() {
fileprivate func requestWrite() {
self._triggerWrite = true
}
@ -118,6 +152,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
if !self.inMemory {
await self.loadFromFile()
} else {
await self._delegate?.loadingForMemoryCollection()
await MainActor.run {
self.setAsLoaded()
}
@ -169,6 +204,19 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
self._updateIndexIfNecessary()
}
@MainActor
func loadAndWrite(_ items: [T], clear: Bool = false) {
if clear {
self.setItems(items)
self.setAsLoaded()
} else {
self.setAsLoaded()
self.addOrUpdate(contentOfs: items)
}
self.requestWrite()
}
/// Updates the whole index with the items array
fileprivate func _updateIndexIfNecessary() {
if self._indexes != nil {
@ -180,76 +228,71 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
/// Adds or updates the provided instance inside the collection
/// Adds it if its id is not found, and otherwise updates it
public func addOrUpdate(instance: T) {
self.addOrUpdateItem(instance: instance)
@discardableResult public func addOrUpdate(instance: T) -> ActionResult<T> {
defer {
self.requestWrite()
}
return self._rawAddOrUpdate(instance: instance)
}
/// Adds or update an instance inside the collection and writes
func addOrUpdateItem(instance: T) {
/// Adds or update a sequence of elements
public func addOrUpdate(contentOfs sequence: any Sequence<T>, _ handler: ((ActionResult<T>) -> ())? = nil) {
defer {
self._triggerWrite = true
self.requestWrite()
}
for instance in sequence {
let result = self._rawAddOrUpdate(instance: instance)
handler?(result)
}
}
fileprivate func _rawAddOrUpdate(instance: T) -> ActionResult<T> {
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.updateItem(instance, index: index)
let updated = self.updateItem(instance, index: index, actionOption: .standard)
return ActionResult(instance: instance, method: .update, pending: !updated)
} else {
self.addItem(instance: instance)
let added = self.addItem(instance: instance)
return ActionResult(instance: instance, method: .insert, pending: !added)
}
}
/// A method the treat the collection as a single instance holder
func setSingletonNoSync(instance: T) {
defer {
self._triggerWrite = true
self.requestWrite()
}
self.items.removeAll()
self.addItem(instance: instance)
}
/// Deletes the instance in the collection and sets the collection as changed to trigger a write
public func delete(instance: T) {
public func delete(instance: T, actionOption: ActionOption) {
defer {
self._triggerWrite = true
}
self.deleteItem(instance)
self.deleteItem(instance, actionOption: actionOption)
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
public func delete(contentOfs sequence: any RandomAccessCollection<T>) {
defer {
self._triggerWrite = true
public func delete(contentOfs sequence: any RandomAccessCollection<T>, _ handler: ((ActionResult<T>) -> ())? = nil) {
self.delete(contentOfs: sequence, actionOption: .cascade, handler: handler)
}
for instance in sequence {
self.deleteItem(instance)
}
}
func delete(contentOfs sequence: any RandomAccessCollection<T>, actionOption: ActionOption, handler: ((ActionResult<T>) -> ())? = nil) {
/// Adds or update a sequence of elements
public func addOrUpdate(contentOfs sequence: any Sequence<T>) {
self.addSequence(sequence)
// self._addOrUpdate(contentOfs: sequence)
}
/// Adds a sequence of objects inside the collection and performs a write
func addSequence(_ sequence: any Sequence<T>) {
defer {
self._triggerWrite = true
}
for instance in sequence {
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.updateItem(instance, index: index)
} else { // insert
self.addItem(instance: instance)
let deleted = self.deleteItem(instance, actionOption: actionOption)
handler?(ActionResult(instance: instance, method: .delete, pending: !deleted))
}
}
}
/// This method sets the storeId for the given instance if the collection belongs to a store with an id
fileprivate func _affectStoreIdIfNecessary(instance: T) {
if let storeId = self.store.identifier {
@ -261,11 +304,16 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
}
func add(instance: T, actionOption: ActionOption) {
self.addItem(instance: instance, actionOption: actionOption)
self.requestWrite()
}
/// Adds an instance to the collection
@discardableResult func addItem(instance: T, shouldBeSynchronized: Bool = false) -> Bool {
@discardableResult fileprivate func addItem(instance: T, actionOption: ActionOption = .standard) -> Bool {
if !self.hasLoaded {
self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
self.addPendingOperation(method: .add, instance: instance, actionOption: actionOption)
return false
}
@ -277,11 +325,16 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
return true
}
func update(_ instance: T, index: Int, actionOption: ActionOption) {
self.updateItem(instance, index: index, actionOption: actionOption)
self.requestWrite()
}
/// Updates an instance to the collection by index
@discardableResult func updateItem(_ instance: T, index: Int, shouldBeSynchronized: Bool = false) -> Bool {
@discardableResult fileprivate func updateItem(_ instance: T, index: Int, actionOption: ActionOption) -> Bool {
if !self.hasLoaded {
self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
self.addPendingOperation(method: .update, instance: instance, actionOption: actionOption)
return false
}
@ -296,15 +349,15 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
/// Deletes an instance from the collection
@discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool {
@discardableResult fileprivate func deleteItem(_ instance: T, actionOption: ActionOption = .cascade) -> Bool {
if !self.hasLoaded {
self.addPendingOperation(method: .delete, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
self.addPendingOperation(method: .delete, instance: instance, actionOption: actionOption)
return false
}
if shouldBeSynchronized { // when user initiated, we want to cascade delete, but when synchronized, we want the delete notifications to make the job and be sure everything works
instance.deleteDependencies(store: self.store, shouldBeSynchronized: shouldBeSynchronized)
if actionOption.cascade { // when user initiated, we want to cascade delete, but when synchronized, we want the delete notifications to make the job and be sure everything works
instance.deleteDependencies(store: self.store, actionOption: actionOption)
}
self.localDeleteOnly(instance: instance)
@ -312,10 +365,10 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
/// Deletes an instance from the collection
@discardableResult func deleteUnusedShared(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool {
@discardableResult func deleteUnusedShared(_ instance: T, actionOption: ActionOption) -> Bool {
if !self.hasLoaded {
self.addPendingOperation(method: .deleteUnusedShared, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
self.addPendingOperation(method: .deleteUnusedShared, instance: instance, actionOption: actionOption)
return false
}
@ -339,6 +392,13 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
}
func deleteByStringId(_ id: String, actionOption: ActionOption = .cascade) {
let realId = T.buildRealId(id: id)
if let instance = self.findById(realId) {
self.deleteItem(instance, actionOption: actionOption)
}
}
/// Returns the instance corresponding to the provided [id]
public func findById(_ id: T.ID) -> T? {
if let index = self._indexes, let instance = index[id] {
@ -361,17 +421,33 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
}
public func deleteAllItemsAndDependencies(actionOption: ActionOption) {
self._delete(contentOfs: self.items, actionOption: actionOption)
}
public func deleteDependencies(actionOption: ActionOption, _ isIncluded: (T) -> Bool) {
let items = self.items.filter(isIncluded)
self._delete(contentOfs: items, actionOption: actionOption)
}
fileprivate func _delete(contentOfs sequence: any RandomAccessCollection<T>, actionOption: ActionOption) {
for instance in sequence {
self.deleteItem(instance, actionOption: actionOption)
}
}
// MARK: - Pending operations
func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) {
func addPendingOperation(method: StorageMethod, instance: T, actionOption: ActionOption) {
if self.pendingOperationManager == nil {
self.pendingOperationManager = PendingOperationManager<T>(store: self.store, inMemory: self.inMemory)
}
self._addPendingOperationIfPossible(method: method, instance: instance, shouldBeSynchronized: false)
self._addPendingOperationIfPossible(method: method, instance: instance, actionOption: actionOption)
}
fileprivate func _addPendingOperationIfPossible(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) {
self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
fileprivate func _addPendingOperationIfPossible(method: StorageMethod, instance: T, actionOption: ActionOption) {
self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, actionOption: actionOption)
}
fileprivate func _mergePendingOperations() {
@ -381,18 +457,16 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
Logger.log(">>> Merge pending: \(manager.items.count)")
for item in manager.items {
let data = item.data
switch (item.method, item.shouldBeSynchronized) {
case (.addOrUpdate, true):
switch item.method {
case .add, .update:
self.addOrUpdate(instance: data)
case (.addOrUpdate, false):
self.addOrUpdateItem(instance: data)
case (.delete, true):
self.delete(instance: data)
case (.delete, false):
self.deleteItem(data)
case (.deleteUnusedShared, _):
self.deleteUnusedShared(data)
case .delete:
self.deleteItem(data, actionOption: item.actionOption)
case .deleteUnusedShared:
self.deleteUnusedShared(data, actionOption: item.actionOption)
}
self._delegate?.itemMerged(item)
}
self.pendingOperationManager = nil
@ -402,10 +476,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
/// Schedules a write operation
fileprivate func _scheduleWrite() {
guard !self.inMemory else { return }
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { // sync to make sure we don't have writes performed at the same time
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait {
self._write()
}
}
@ -431,7 +502,6 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
public func reset() {
self.items.removeAll()
self.store.removeFile(type: T.self)
triggerWrite()
}
public var type: any Storable.Type { return T.self }
@ -451,38 +521,23 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
}
}
}
public class StoredCollection<T: Storable>: BaseCollection<T>, RandomAccessCollection {
// MARK: - for Synced Collection
/// Returns a dummy StoredCollection instance
public static func placeholder() -> StoredCollection<T> {
return StoredCollection<T>(store: Store(storeCenter: StoreCenter.main))
}
// MARK: - RandomAccessCollection
public var startIndex: Int { return self.items.startIndex }
public var endIndex: Int { return self.items.endIndex }
public func index(after i: Int) -> Int {
return self.items.index(after: i)
}
open subscript(index: Int) -> T {
get {
return self.items[index]
}
set(newValue) {
self.items[index] = newValue
self._triggerWrite = true
@MainActor
func updateLocalInstance(_ serverInstance: T) {
if let localInstance = self.findById(serverInstance.id) {
localInstance.copy(from: serverInstance)
self.requestWrite()
}
}
}
extension SyncedCollection: RandomAccessCollection {
extension StoredCollection: RandomAccessCollection {
public static func placeholder() -> StoredCollection<T> {
return StoredCollection<T>(store: Store(storeCenter: StoreCenter.main))
}
public var startIndex: Int { return self.items.startIndex }
@ -501,4 +556,5 @@ extension SyncedCollection: RandomAccessCollection {
self._triggerWrite = true
}
}
}

@ -25,7 +25,7 @@ public class StoredSingleton<T: SyncedStorable>: SyncedCollection<T> {
/// Sets the singleton to the collection without synchronizing it
public func setItemNoSync(_ instance: T) {
self.setSingletonNoSync(instance: instance)
self.collection.setSingletonNoSync(instance: instance)
}
/// updates the existing singleton
@ -37,7 +37,7 @@ public class StoredSingleton<T: SyncedStorable>: SyncedCollection<T> {
/// Returns the singleton
public func item() -> T? {
return self.items.first
return self.collection.items.first
}
public func tryPutBeforeUpdating(_ instance: T) async throws {

@ -12,7 +12,30 @@ protocol SomeSyncedCollection: SomeCollection {
func loadCollectionsFromServerIfNoFile() async throws
}
public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSyncedCollection {
public class SyncedCollection<T : SyncedStorable>: SomeSyncedCollection, CollectionDelegate {
public typealias Item = T
let store: Store
let collection: StoredCollection<T>
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) {
self.store = store
self.collection = StoredCollection<T>(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading)
}
init(store: Store, inMemory: Bool) async {
self.store = store
self.collection = await StoredCollection(store: store, inMemory: inMemory)
}
var storeCenter: StoreCenter { return self.store.storeCenter }
public var storeId: String? {
return self.store.identifier
}
/// Returns a dummy SyncedCollection instance
public static func placeholder() -> SyncedCollection<T> {
@ -20,17 +43,17 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
/// Migrates if necessary and asynchronously decodes the json file
override func load() async {
do {
if self.inMemory {
try await self.loadDataFromServerIfAllowed()
} else {
await self.loadFromFile()
}
} catch {
Logger.error(error)
}
}
// override func load() async {
// do {
// if self.inMemory {
// try await self.loadDataFromServerIfAllowed()
// } else {
// await self.loadFromFile()
// }
// } catch {
// Logger.error(error)
// }
// }
/// Loads the collection using the server data only if the collection file doesn't exists
func loadCollectionsFromServerIfNoFile() async throws {
@ -64,90 +87,56 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
Task {
await _updateLocalInstance(serverInstance)
await self.collection.updateLocalInstance(serverInstance)
}
// DispatchQueue.main.async {
// if let localInstance = self.findById(serverInstance.id) {
// localInstance.copy(from: serverInstance)
// self.setChanged()
// }
// }
}
@MainActor
fileprivate func _updateLocalInstance(_ serverInstance: T) {
if let localInstance = self.findById(serverInstance.id) {
localInstance.copy(from: serverInstance)
self.triggerWrite()
}
}
@MainActor
func loadItems(_ items: [T], clear: Bool = false) {
if clear {
self.setItems(items)
self.setAsLoaded()
} else {
self.setAsLoaded()
self.addOrUpdateNoSync(contentOfs: items)
}
self.triggerWrite()
self.collection.loadAndWrite(items, clear: clear)
}
// MARK: - Basic operations with sync
/// Adds or update an instance synchronously, dispatching network operations to background tasks
public override func addOrUpdate(instance: T) {
if let result = _addOrUpdateCore(instance: instance) {
if result.isNewItem {
Task { await self._sendInsertion(result.item) }
public func addOrUpdate(instance: T) {
let result = _addOrUpdateCore(instance: instance)
if result.method == .insert {
Task { await self._sendInsertion(instance) }
} else {
Task { await self._sendUpdate(result.item) }
}
Task { await self._sendUpdate(instance) }
}
}
/// Private helper function that contains the shared logic
private func _addOrUpdateCore(instance: T) -> (item: T, isNewItem: Bool)? {
private func _addOrUpdateCore(instance: T) -> ActionResult<T> {
instance.lastUpdate = Date()
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
self.triggerWrite()
let result = self.collection.addOrUpdate(instance: instance)
if result.method == .update {
if instance.shared == true {
self._cleanUpSharedDependencies()
}
return (instance, false)
}
} else {
if self.addItem(instance: instance, shouldBeSynchronized: true) {
self.triggerWrite()
return (instance, true)
}
}
return nil
return result
}
fileprivate func _addOrUpdateCore(contentOfs sequence: any Sequence<T>) -> OperationBatch<T> {
defer {
self.triggerWrite()
}
let date = Date()
let batch = OperationBatch<T>()
for instance in sequence {
instance.lastUpdate = date
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
batch.addUpdate(instance)
}
} else { // insert
if self.addItem(instance: instance, shouldBeSynchronized: true) {
let result = self.collection.addOrUpdate(instance: instance)
if result.method == .insert {
batch.addInsert(instance)
}
} else {
batch.addUpdate(instance)
}
}
@ -158,48 +147,47 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
/// Adds or update a sequence and writes
override public func addOrUpdate(contentOfs sequence: any Sequence<T>) {
public func addOrUpdate(contentOfs sequence: any Sequence<T>) {
let batch = self._addOrUpdateCore(contentOfs: sequence)
Task { await self._sendOperationBatch(batch) }
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
fileprivate func _deleteCore(contentOfs sequence: any RandomAccessCollection<T>) -> OperationBatch<T> {
defer {
self.triggerWrite()
}
var deleted: [T] = []
/// Deletes an instance and writes
public func delete(instance: T) {
for instance in sequence {
if self.deleteItem(instance, shouldBeSynchronized: true) {
deleted.append(instance)
}
self.collection.delete(instance: instance, actionOption: .syncedCascade)
self.storeCenter.createDeleteLog(instance)
}
Task { await self._sendDeletion(instance) }
let batch = OperationBatch<T>()
batch.deletes = deleted
return batch
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
public override func delete(contentOfs sequence: any RandomAccessCollection<T>) {
public func delete(contentOfs sequence: any RandomAccessCollection<T>) {
self.delete(contentOfs: sequence, actionOption: .syncedCascade)
}
func delete(contentOfs sequence: any RandomAccessCollection<T>, actionOption: ActionOption) {
guard sequence.isNotEmpty else { return }
let batch = self._deleteCore(contentOfs: sequence)
let batch = self._deleteCore(contentOfs: sequence, actionOption: actionOption)
if actionOption.synchronize {
Task { await self._sendOperationBatch(batch) }
}
}
/// Deletes an instance and writes
override public func delete(instance: T) {
defer {
self.triggerWrite()
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
fileprivate func _deleteCore(contentOfs sequence: any RandomAccessCollection<T>, actionOption: ActionOption) -> OperationBatch<T> {
var deleted: [T] = []
self.collection.delete(contentOfs: sequence, actionOption: actionOption) { result in
self.storeCenter.createDeleteLog(result.instance)
if !result.pending {
deleted.append(result.instance)
}
}
self.deleteItem(instance, shouldBeSynchronized: true)
self.storeCenter.createDeleteLog(instance)
Task { await self._sendDeletion(instance) }
let batch = OperationBatch<T>()
batch.deletes = deleted
return batch
}
/// Deletes an instance without writing, logs the operation and sends an API call
@ -209,30 +197,33 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
//// await self._sendDeletion(instance)
// }
public func deleteDependencies(_ items: any RandomAccessCollection<T>, shouldBeSynchronized: Bool) {
guard items.isNotEmpty else { return }
if shouldBeSynchronized {
self.delete(contentOfs: items)
} else {
self.deleteNoSync(contentOfs: items)
}
}
public override func deleteDependencies(_ items: any Sequence<T>) {
super.deleteDependencies(items)
let batch = OperationBatch<T>()
batch.deletes = Array(items)
Task { await self._sendOperationBatch(batch) }
}
// public func deleteDependencies(_ items: any RandomAccessCollection<T>, actionOption: ActionOption) {
// guard items.isNotEmpty else { return }
// if actionOption.synchronize {
// self.delete(contentOfs: items)
// } else {
// self.deleteNoSync(contentOfs: items)
// }
// }
public func deleteDependenciesAsync(_ items: any Sequence<T>) async {
super.deleteDependencies(items)
// public func deleteDependencies(_ items: any Sequence<T>) {
//
// self.collection.deleteDependencies(items)
//
//// super.deleteDependencies(items)
//
// let batch = OperationBatch<T>()
// batch.deletes = Array(items)
// Task { await self._sendOperationBatch(batch) }
// }
let batch = OperationBatch<T>()
batch.deletes = Array(items)
await self._sendOperationBatch(batch)
}
// public func deleteDependenciesAsync(_ items: any Sequence<T>) async {
// super.deleteDependencies(items)
//
// let batch = OperationBatch<T>()
// batch.deletes = Array(items)
// await self._sendOperationBatch(batch)
// }
fileprivate func _cleanUpSharedDependencies() {
for relationship in T.relationships() {
@ -261,22 +252,38 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
fileprivate func _deleteUnusedSharedInstances() {
let sharedItems = self.items.filter { $0.shared == true }
let sharedItems = self.collection.items.filter { $0.shared == true }
for sharedItem in sharedItems {
self.store.deleteUnusedSharedIfNecessary(sharedItem)
}
}
public func deleteAllItemsAndDependencies(actionOption: ActionOption) {
if actionOption.synchronize {
self.delete(contentOfs: self.items, actionOption: actionOption)
} else {
self.collection.deleteAllItemsAndDependencies(actionOption: actionOption)
}
}
public func deleteDependencies(actionOption: ActionOption, _ isIncluded: (T) -> Bool) {
let items = self.items.filter(isIncluded)
if actionOption.synchronize {
self.delete(contentOfs: items, actionOption: actionOption)
} else {
self.collection.delete(contentOfs: items)
}
}
// MARK: - Asynchronous operations
/// Adds or update an instance asynchronously and waits for network operations
func addOrUpdateAsync(instance: T) async throws {
if let result = _addOrUpdateCore(instance: instance) {
if result.isNewItem {
try await self._executeBatchOnce(OperationBatch(insert: result.item))
let result = _addOrUpdateCore(instance: instance)
if result.method == .insert {
try await self._executeBatchOnce(OperationBatch(insert: instance))
} else {
try await self._executeBatchOnce(OperationBatch(update: result.item))
}
try await self._executeBatchOnce(OperationBatch(update: instance))
}
}
@ -288,16 +295,13 @@ 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
public func deleteAsync(contentOfs sequence: any RandomAccessCollection<T>) async throws {
guard sequence.isNotEmpty else { return }
let batch = self._deleteCore(contentOfs: sequence)
let batch = self._deleteCore(contentOfs: sequence, actionOption: .syncedCascade)
try await self._executeBatchOnce(batch)
}
/// Deletes an instance and writes
func deleteAsync(instance: T) async throws {
defer {
self.triggerWrite()
}
self.deleteItem(instance, shouldBeSynchronized: true)
self.collection.delete(instance: instance, actionOption: .syncedCascade)
self.storeCenter.createDeleteLog(instance)
try await self._executeBatchOnce(OperationBatch(delete: instance))
}
@ -306,51 +310,63 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Adds or update an instance without synchronizing it
func addOrUpdateNoSync(_ instance: T) {
self.addOrUpdateItem(instance: instance)
self.collection.addOrUpdate(instance: instance)
// self.addOrUpdateItem(instance: instance)
}
/// Adds or update a sequence of elements without synchronizing it
func addOrUpdateNoSync(contentOfs sequence: any Sequence<T>) {
self.addSequence(sequence)
self.collection.addOrUpdate(contentOfs: sequence)
}
/// Deletes the instance in the collection without synchronization
func deleteNoSync(contentOfs sequence: any Sequence<T>) {
defer {
self.triggerWrite()
}
for item in sequence {
self.deleteItem(item, shouldBeSynchronized: false)
}
}
// func deleteNoSync(instance: T) {
// self.collection.delete(instance: instance)
// }
/// Deletes the instance in the collection without synchronization
func deleteNoSync(instance: T) {
defer {
self.triggerWrite()
}
self.deleteItem(instance, shouldBeSynchronized: false)
func deleteNoSync(contentOfs sequence: any RandomAccessCollection<T>) {
self.collection.delete(contentOfs: sequence)
}
func deleteUnusedShared(instance: T) {
guard instance.shared == true else { return }
// Delete the instance and its non-used shared dependencies
self.deleteUnusedShared(instance, shouldBeSynchronized: false)
self.triggerWrite()
self.delete(instance: instance)
instance.deleteUnusedSharedDependencies(store: self.store)
}
/// Deletes the instance in the collection without synchronization
func deleteByStringIdNoSync(_ id: String) {
defer {
self.triggerWrite()
func deleteNoSyncNoCascade(id: String) {
self.collection.deleteByStringId(id, actionOption: .standard)
}
let realId = T.buildRealId(id: id)
if let instance = self.findById(realId) {
self.deleteItem(instance, shouldBeSynchronized: false)
// MARK: - Collection Delegate
func loadingForMemoryCollection() async {
do {
try await self.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
}
}
func itemMerged(_ pendingOperation: PendingOperation<T>) {
let batch = OperationBatch<T>()
switch pendingOperation.method {
case .add:
batch.inserts.append(pendingOperation.data)
case .update:
batch.updates.append(pendingOperation.data)
case .delete:
batch.deletes.append(pendingOperation.data)
case .deleteUnusedShared:
break
}
Task { await self._sendOperationBatch(batch) }
}
// MARK: - Send requests
@ -397,14 +413,15 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Adds or update an instance if it is newer than the local instance
func addOrUpdateIfNewer(_ instance: T, shared: Bool) {
defer {
self.triggerWrite()
}
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
let localInstance = self.items[index]
// defer {
// self.triggerWrite()
// }
if let index = self.collection.items.firstIndex(where: { $0.id == instance.id }) {
let localInstance = self.collection.items[index]
if instance.lastUpdate > localInstance.lastUpdate {
self.updateItem(instance, index: index)
self.collection.update(instance, index: index, actionOption: .standard)
} else {
print("do not update \(T.resourceName()): \(instance.lastUpdate.timeIntervalSince1970) / local: \(localInstance.lastUpdate.timeIntervalSince1970)")
}
@ -412,7 +429,7 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
if shared {
instance.shared = true
}
self.addItem(instance: instance, shouldBeSynchronized: false)
self.collection.add(instance: instance, actionOption: .standard)
}
}
@ -421,12 +438,35 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
/// Sends a POST request for the instance, and changes the collection to perform a write
public func writeChangeAndInsertOnServer(instance: T) {
self.collection.addOrUpdate(instance: instance)
Task {
await self._sendInsertion(instance)
await MainActor.run {
self.triggerWrite()
}
}
// MARK: - SomeCollection
public var hasLoaded: Bool { return self.collection.hasLoaded}
public var inMemory: Bool { return self.collection.inMemory }
public var type: any Storable.Type { return T.self }
public func referenceCount<S>(type: S.Type, id: String) -> Int where S : Storable {
return self.collection.referenceCount(type: type, id: id)
}
public func reset() {
self.collection.reset()
}
public func findById(_ id: T.ID) -> T? {
return self.collection.findById(id)
}
public var items: [T] {
return self.collection.items
}
}
@ -459,3 +499,25 @@ class OperationBatch<T> {
self.deletes.append(instance)
}
}
extension SyncedCollection: RandomAccessCollection {
public var startIndex: Int { return self.collection.items.startIndex }
public var endIndex: Int { return self.collection.items.endIndex }
public func index(after i: Int) -> Int {
return self.collection.items.index(after: i)
}
public subscript(index: Int) -> T {
get {
return self.collection.items[index]
}
set(newValue) {
self.collection.update(newValue, index: index, actionOption: .standard)
// self.collection.items[index] = newValue
// self._triggerWrite = true
}
}
}

@ -30,13 +30,7 @@ public protocol SideStorable {
var storeId: String? { get set }
}
public extension SyncedStorable {
func copy() -> Self {
let copy = Self()
copy.copy(from: self)
return copy
}
extension Storable {
func getStoreId() -> String? {
if let alt = self as? SideStorable {
@ -46,3 +40,13 @@ public extension SyncedStorable {
}
}
public extension SyncedStorable {
func copy() -> Self {
let copy = Self()
copy.copy(from: self)
return copy
}
}

@ -0,0 +1,17 @@
//
// String+Extensions.swift
// LeStorage
//
// Created by Laurent Morvillier on 19/05/2025.
//
import Foundation
public extension String {
static func random(length: Int = 10) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}

@ -55,7 +55,8 @@ struct IdentifiableTests {
let stringObjects: StoredCollection<StringObject>
init() {
let storeCenter = StoreCenter.main
let dir = "test_" + String.random()
let storeCenter: StoreCenter = StoreCenter(directoryName:dir)
intObjects = storeCenter.mainStore.registerCollection()
stringObjects = storeCenter.mainStore.registerCollection()
}

@ -67,7 +67,7 @@ struct StoredCollectionTests {
let item = MockStorable(id: "1", name: "Test")
collection.addOrUpdate(instance: item)
collection.deleteById("1")
collection.deleteByStringId("1")
let search = collection.findById("1")
#expect(search == nil)
}

Loading…
Cancel
Save