Cleanup unused stuff + Renamed StoredObject and convenience methods

multistore
Laurent 2 years ago
parent 2673b6e58d
commit 0c255cc256
  1. 8
      LeStorage.xcodeproj/project.pbxproj
  2. 4
      LeStorage/MicroStorage.swift
  3. 4
      LeStorage/Services.swift
  4. 100
      LeStorage/Store.swift
  5. 63
      LeStorage/StoredCollection.swift
  6. 7
      LeStorage/StoredSingleton.swift
  7. 152
      LeStorage/Wip/Migration.swift

@ -12,7 +12,7 @@
C425D4442B6D24E1002A7B48 /* LeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */; };
C425D4452B6D24E1002A7B48 /* LeStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C425D4372B6D24E1002A7B48 /* LeStorage.h */; settings = {ATTRIBUTES = (Public, ); }; };
C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; };
C456EFE22BE52379007388E2 /* StoredObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredObject.swift */; };
C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredSingleton.swift */; };
C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.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 */; };
@ -48,7 +48,7 @@
C425D43E2B6D24E1002A7B48 /* LeStorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeStorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeStorageTests.swift; sourceTree = "<group>"; };
C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
C456EFE12BE52379007388E2 /* StoredObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredObject.swift; sourceTree = "<group>"; };
C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = "<group>"; };
C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.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>"; };
@ -116,7 +116,7 @@
C425D4572B6D2519002A7B48 /* Store.swift */,
C4A47D642B6E92FE00ADC637 /* Storable.swift */,
C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */,
C456EFE12BE52379007388E2 /* StoredObject.swift */,
C456EFE12BE52379007388E2 /* StoredSingleton.swift */,
C4A47D932B7CF7C500ADC637 /* MicroStorage.swift */,
C4A47D822B7665BC00ADC637 /* Wip */,
C4A47D582B6D352900ADC637 /* Utils */,
@ -280,7 +280,7 @@
C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */,
C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */,
C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */,
C456EFE22BE52379007388E2 /* StoredObject.swift in Sources */,
C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */,
C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */,
C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */,

@ -35,10 +35,10 @@ public class MicroStorage<T : MicroStorable> {
public func update(handler: (T) -> ()) {
handler(self.item)
self._write()
self.write()
}
fileprivate func _write() {
func write() {
do {
let jsonString: String = try self.item.jsonString()
let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: T.fileName)

@ -146,7 +146,7 @@ public class Services {
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if !(requiresToken == false), let token = try? self.keychainStore.getToken() {
Logger.log("current token = \(token)")
// Logger.log("current token = \(token)")
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
}
@ -182,7 +182,7 @@ public class Services {
do {
let token = try self.keychainStore.getToken()
Logger.log("current token = \(token)")
// Logger.log("current token = \(token)")
request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
} catch {
Logger.log("missing token")

@ -51,19 +51,18 @@ public class Store {
/// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:]
// /// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
// fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
/// The list of migrations to apply
fileprivate var _migrations: [SomeMigration] = []
/// The collection of performed migration on the store
fileprivate lazy var _migrationCollection: StoredCollection<MigrationHistory> = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false)
}()
fileprivate var settingsStorage: MicroStorage<Settings> = MicroStorage()
/// A store for the Settings object
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage()
/// The name of the directory to store the json files
static let storageDirectory = "storage"
/// Indicates to Stored Collection if they can synchronize
public var collectionsCanSynchronize: Bool = true {
didSet {
Logger.log(">>> collectionsCanSynchronize = \(self.collectionsCanSynchronize)")
}
}
public init() {
FileManager.default.createDirectoryInDocuments(directoryName: Store.storageDirectory)
@ -87,10 +86,10 @@ public class Store {
return collection
}
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredObject<T> {
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredSingleton<T> {
// register collection
let storedObject = StoredObject<T>(synchronized: synchronized, store: Store.main, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
let storedObject = StoredSingleton<T>(synchronized: synchronized, store: Store.main, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
self._collections[T.resourceName()] = storedObject
return storedObject
@ -99,18 +98,25 @@ public class Store {
// MARK: - Settings
func setUserUUID(uuidString: String) {
self.settingsStorage.update { settings in
self._settingsStorage.update { settings in
settings.userId = uuidString
}
}
public func currentUserUUID() -> UUID {
if let uuidString = self.settingsStorage.item.userId,
let uuid = UUID(uuidString: uuidString) {
public var currentUserUUID: UUID? {
if let uuidString = self._settingsStorage.item.userId,
let uuid = UUID(uuidString: uuidString) {
return uuid
}
return nil
}
public func mandatoryUserUUID() -> UUID {
if let uuid = self.currentUserUUID {
return uuid
} else {
let uuid = UIDevice.current.identifierForVendor ?? UUID()
self.settingsStorage.update { settings in
self._settingsStorage.update { settings in
settings.userId = uuid.uuidString
}
return uuid
@ -118,18 +124,18 @@ public class Store {
}
func userName() -> String? {
return self.settingsStorage.item.username
return self._settingsStorage.item.username
}
func setUserName(_ username: String) {
self.settingsStorage.update { settings in
self._settingsStorage.update { settings in
settings.username = username
}
}
public func disconnect(resetAll: Bool = false) {
try? self.service().disconnect()
self.settingsStorage.update { settings in
self._settingsStorage.update { settings in
settings.username = nil
settings.userId = nil
}
@ -182,47 +188,7 @@ public class Store {
public func deleteDependencies<T: Storable>(items: any Sequence<T>) throws {
try self.collection().deleteDependencies(items)
}
// MARK: - Migration
/// [beta] Adds a migration to perform
public func addMigration(_ migration: SomeMigration) {
self._migrations.append(migration)
}
/// [beta] Performs the migration if necessary
func performMigrationIfNecessary<T: Storable>(_ collection: StoredCollection<T>) async throws {
// Check for migrations
let migrations = self._migrations.filter { $0.resourceName == T.resourceName() }
if migrations.isEmpty { return }
// Check for applied migrations
var version: Int = -1
var performedMigration: MigrationHistory? = self._migrationCollection.first(where: { $0.resourceName == T.resourceName() })
if let performedMigration {
version = Int(performedMigration.version)
}
// Apply necessary migrations
let applicableMigrations = migrations.filter { $0.version > version }
.sorted(keyPath: \.version)
for migration in applicableMigrations {
Logger.log("Start migration for \(migration.resourceName), version: \(migration.version)")
try migration.migrate(synchronized: collection.synchronized)
if let performedMigration {
performedMigration.version = migration.version
} else {
performedMigration = MigrationHistory(version: migration.version, resourceName: migration.resourceName)
}
try self._migrationCollection.addOrUpdate(instance: performedMigration!)
}
}
// MARK: - Api call rescheduling
/// Deletes an ApiCall by [id] and [collectionName]
@ -236,6 +202,9 @@ public class Store {
/// Reschedule an ApiCall by id
func rescheduleApiCall<T: Storable>(id: String, type: T.Type) throws {
guard self.collectionsCanSynchronize else {
return
}
let collection: StoredCollection<T> = try self.collection()
collection.rescheduleApiCallsIfNecessary()
}
@ -245,11 +214,6 @@ public class Store {
return try await self.service().runApiCall(apiCall)
}
/// Executes an ApiCall
// func execute<T>(apiCall: ApiCall<T>) async throws {
// _ = try await self._executeApiCall(apiCall)
// }
func execute<T>(apiCall: ApiCall<T>) async throws -> T {
return try await self._executeApiCall(apiCall)
}
@ -265,9 +229,9 @@ public class Store {
}
}
public func loadCollections() {
public func loadCollectionFromServer() {
for collection in self._collections.values {
try? collection.loadDataFromServer()
try? collection.loadDataFromServerIfAllowed()
}
}

@ -18,9 +18,10 @@ protocol SomeCollection: Identifiable {
func deleteById(_ id: String) throws
func deleteApiCallById(_ id: String) throws
func reset()
func loadDataFromServer() throws
func loadDataFromServerIfAllowed() throws
}
extension Notification.Name {
public static let CollectionDidLoad: Notification.Name = Notification.Name.init("notification.collectionDidLoad")
public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange")
@ -102,9 +103,9 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
try content.write(to: fileURL, atomically: false, encoding: .utf8)
}
fileprivate func _pathForFile(_ fileName: String) throws -> URL {
fileprivate func _urlForJSONFile() throws -> URL {
var storageDirectory = try self._storageDirectoryPath()
storageDirectory.append(component: fileName)
storageDirectory.append(component: T.fileName())
return storageDirectory
}
@ -115,7 +116,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
do {
if self._inMemory {
try self.loadDataFromServer()
try self.loadDataFromServerIfAllowed()
} else {
try self._loadFromFile()
}
@ -126,12 +127,12 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
}
fileprivate func _loadFromFile() throws {
let url: URL = try self._pathForFile(T.fileName())
let url: URL = try self._urlForJSONFile()
if FileManager.default.fileExists(atPath: url.path()) {
if self.asynchronousIO {
Task(priority: .high) {
try await Store.main.performMigrationIfNecessary(self)
// try await Store.main.performMigrationIfNecessary(self)
try self._decodeJSONFile()
}
} else {
@ -143,7 +144,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Decodes the json file into the items array
fileprivate func _decodeJSONFile() throws {
let fileURL = try self._pathForFile(T.fileName())
let fileURL = try self._urlForJSONFile()
let jsonString: String = try FileUtils.readFile(fileURL: fileURL)
if let decoded: [T] = try jsonString.decodeArray() {
DispatchQueue.main.async {
@ -165,13 +166,14 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
}
/// Retrieves the data from the server and loads it into the items array
public func loadDataFromServer() throws {
guard self.synchronized else {
public func loadDataFromServerIfAllowed() throws {
guard self.synchronized, !(self is StoredSingleton<T>) else {
throw StoreError.unSynchronizedCollection
}
Task {
do {
self.items = try await self._store.getItems()
let items: [T] = try await self._store.getItems()
try self._addOrUpdate(contentOfs: items, shouldSync: false)
self._hasChanged = true
} catch {
Logger.error(error)
@ -201,6 +203,14 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
}
func setSingletonNoSync(instance: T) {
defer {
self._hasChanged = true
}
self.items.removeAll()
self.items.append(instance)
}
/// Deletes the instance in the collection by id
public func delete(instance: T) throws {
@ -230,8 +240,12 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
}
}
/// Inserts or updates all items in the sequence
public func addOrUpdate(contentOfs sequence: any Sequence<T>) throws {
try self._addOrUpdate(contentOfs: sequence)
}
/// Inserts or updates all items in the sequence
fileprivate func _addOrUpdate(contentOfs sequence: any Sequence<T>, shouldSync: Bool = true) throws {
defer {
self._hasChanged = true
}
@ -239,11 +253,15 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
for instance in sequence {
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
try self._sendUpdateIfNecessary(instance)
if shouldSync {
try self._sendUpdateIfNecessary(instance)
}
} else { // insert
self.items.append(instance)
self._index?[instance.stringId] = instance
try self._sendInsertionIfNecessary(instance)
if shouldSync {
try self._sendInsertionIfNecessary(instance)
}
}
}
@ -307,9 +325,20 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
Logger.log("End write")
}
func clear() {
self.items.removeAll()
}
func reset() {
self.items.removeAll()
try? FileUtils.removeFileFromDocumentDirectory(fileName: T.fileName())
do {
let url: URL = try self._urlForJSONFile()
try FileManager.default.removeItem(at: url)
} catch {
Logger.error(error)
}
if let apiCallsCollection = self.apiCallsCollection {
apiCallsCollection.reset()
}
@ -357,7 +386,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Sends an insert api call for the provided [instance]
fileprivate func _sendInsertionIfNecessary(_ instance: T) throws {
guard self.synchronized else {
guard self.synchronized, Store.main.collectionsCanSynchronize else {
return
}
@ -379,7 +408,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Sends an update api call for the provided [instance]
fileprivate func _sendUpdateIfNecessary(_ instance: T) throws {
guard self.synchronized, self._sendsUpdate else {
guard self.synchronized, self._sendsUpdate, Store.main.collectionsCanSynchronize else {
return
}
@ -399,7 +428,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Sends an delete api call for the provided [instance]
fileprivate func _sendDeletionIfNecessary(_ instance: T) throws {
guard self.synchronized else {
guard self.synchronized, Store.main.collectionsCanSynchronize else {
return
}

@ -7,11 +7,10 @@
import Foundation
public class StoredObject<T: Storable>: StoredCollection<T> {
public class StoredSingleton<T: Storable>: StoredCollection<T> {
public func setItem(_ instance: T) throws {
self.reset()
try self.addOrUpdate(instance: instance)
public func setItemNoSync(_ instance: T) throws {
self.setSingletonNoSync(instance: instance)
}
public func update() throws {

@ -7,79 +7,79 @@
import Foundation
public protocol MigrationSource : Storable {
associatedtype Destination : Storable
func migrate() -> Destination
}
public protocol SomeMigration {
func migrate(synchronized: Bool) throws
var version: UInt { get }
var resourceName: String { get }
}
public class Migration<S : MigrationSource, D : Storable> : SomeMigration where S.Destination == D {
public var version: UInt
public var resourceName: String {
return S.resourceName()
}
public init(version: UInt) {
self.version = version
}
public func migrate(synchronized: Bool) throws {
try self._migrateMainCollection()
if synchronized {
try self._migrateApiCallsCollection()
}
}
fileprivate func _migrateMainCollection() throws {
let jsonString = try FileUtils.readDocumentFile(fileName: S.fileName())
if let decoded: [S] = try jsonString.decodeArray() {
let migratedObjects: [D] = decoded.map { $0.migrate() }
let jsonString: String = try migratedObjects.jsonString()
let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: D.fileName())
}
}
fileprivate func _migrateApiCallsCollection() throws {
let jsonString = try FileUtils.readDocumentFile(fileName: ApiCall<S>.fileName())
if let apiCalls: [ApiCall<S>] = try jsonString.decodeArray() {
let migratedCalls = try apiCalls.map { apiCall in
if let source: S = try apiCall.body.decode() {
let migrated: D = source.migrate()
apiCall.body = try migrated.jsonString()
}
return apiCall
}
let jsonString = try migratedCalls.jsonString()
let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: ApiCall<D>.fileName())
Logger.log("Ended _migrateApiCallsCollection: \(jsonString)")
}
}
}
class MigrationHistory: ModelObject, Storable {
public static func resourceName() -> String { "migration_history" }
public var id: String = Store.randomId()
public var version: UInt
public var resourceName: String
init(version: UInt, resourceName: String) {
self.version = version
self.resourceName = resourceName
}
}
//public protocol MigrationSource : Storable {
// associatedtype Destination : Storable
// func migrate() -> Destination
//}
//
//public protocol SomeMigration {
// func migrate(synchronized: Bool) throws
// var version: UInt { get }
// var resourceName: String { get }
//}
//
//public class Migration<S : MigrationSource, D : Storable> : SomeMigration where S.Destination == D {
//
// public var version: UInt
//
// public var resourceName: String {
// return S.resourceName()
// }
//
// public init(version: UInt) {
// self.version = version
// }
//
// public func migrate(synchronized: Bool) throws {
// try self._migrateMainCollection()
// if synchronized {
// try self._migrateApiCallsCollection()
// }
// }
//
// fileprivate func _migrateMainCollection() throws {
// let jsonString = try FileUtils.readDocumentFile(fileName: S.fileName())
// if let decoded: [S] = try jsonString.decodeArray() {
//
// let migratedObjects: [D] = decoded.map { $0.migrate() }
//
// let jsonString: String = try migratedObjects.jsonString()
// let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: D.fileName())
// }
// }
//
// fileprivate func _migrateApiCallsCollection() throws {
// let jsonString = try FileUtils.readDocumentFile(fileName: ApiCall<S>.fileName())
// if let apiCalls: [ApiCall<S>] = try jsonString.decodeArray() {
//
// let migratedCalls = try apiCalls.map { apiCall in
// if let source: S = try apiCall.body.decode() {
// let migrated: D = source.migrate()
// apiCall.body = try migrated.jsonString()
// }
// return apiCall
// }
//
// let jsonString = try migratedCalls.jsonString()
// let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: ApiCall<D>.fileName())
//
// Logger.log("Ended _migrateApiCallsCollection: \(jsonString)")
// }
// }
//
//}
//
//class MigrationHistory: ModelObject, Storable {
//
// public static func resourceName() -> String { "migration_history" }
//
// public var id: String = Store.randomId()
// public var version: UInt
// public var resourceName: String
//
// init(version: UInt, resourceName: String) {
// self.version = version
// self.resourceName = resourceName
// }
//
//}

Loading…
Cancel
Save