diff --git a/FlatStore.xcodeproj/project.pbxproj b/FlatStore.xcodeproj/project.pbxproj index 078bc6a..5cd09d7 100644 --- a/FlatStore.xcodeproj/project.pbxproj +++ b/FlatStore.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 4B24C5582196021300932155 /* FlatBatchUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B24C5552196021100932155 /* FlatBatchUpdate.swift */; }; 4B24C5592196021300932155 /* FlatRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B24C5562196021200932155 /* FlatRef.swift */; }; 4B59A5042337E49F0088E921 /* FlatStore+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59A5032337E49F0088E921 /* FlatStore+Identifiable.swift */; }; + 4B68395223708286002FFC5A /* InMemoryEntityStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B68395123708286002FFC5A /* InMemoryEntityStorage.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,6 +40,7 @@ 4B24C5552196021100932155 /* FlatBatchUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlatBatchUpdate.swift; sourceTree = ""; }; 4B24C5562196021200932155 /* FlatRef.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlatRef.swift; sourceTree = ""; }; 4B59A5032337E49F0088E921 /* FlatStore+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FlatStore+Identifiable.swift"; sourceTree = ""; }; + 4B68395123708286002FFC5A /* InMemoryEntityStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryEntityStorage.swift; sourceTree = ""; }; 4BF9E4BA21A0202A006AE786 /* Playground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Playground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; /* End PBXFileReference section */ @@ -87,6 +89,7 @@ 4B24C5552196021100932155 /* FlatBatchUpdate.swift */, 4B24C5562196021200932155 /* FlatRef.swift */, 4B24C5542196021100932155 /* FlatStore.swift */, + 4B68395123708286002FFC5A /* InMemoryEntityStorage.swift */, 4B59A5032337E49F0088E921 /* FlatStore+Identifiable.swift */, 4B24C53E219600B000932155 /* Info.plist */, ); @@ -213,6 +216,7 @@ buildActionMask = 2147483647; files = ( 4B24C5582196021300932155 /* FlatBatchUpdate.swift in Sources */, + 4B68395223708286002FFC5A /* InMemoryEntityStorage.swift in Sources */, 4B24C5592196021300932155 /* FlatRef.swift in Sources */, 4B59A5042337E49F0088E921 /* FlatStore+Identifiable.swift in Sources */, 4B24C5572196021200932155 /* FlatStore.swift in Sources */, diff --git a/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/03A8D90B-CE32-4076-A881-2DAE0795E48B.plist b/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/03A8D90B-CE32-4076-A881-2DAE0795E48B.plist new file mode 100644 index 0000000..7de6c6c --- /dev/null +++ b/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/03A8D90B-CE32-4076-A881-2DAE0795E48B.plist @@ -0,0 +1,22 @@ + + + + + classNames + + FlatStoreTests + + testPerformanceExample() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.073941 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/Info.plist b/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/Info.plist index 53282ee..b6147cc 100644 --- a/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/Info.plist +++ b/FlatStore.xcodeproj/xcshareddata/xcbaselines/4B24C542219600B000932155.xcbaseline/Info.plist @@ -4,6 +4,37 @@ runDestinationsByUUID + 03A8D90B-CE32-4076-A881-2DAE0795E48B + + localComputer + + busSpeedInMHz + 400 + cpuCount + 1 + cpuKind + 6-Core Intel Core i9 + cpuSpeedInMHz + 2900 + logicalCPUCoresPerPackage + 12 + modelCode + MacBookPro15,3 + physicalCPUCoresPerPackage + 6 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone12,5 + platformIdentifier + com.apple.platform.iphonesimulator + + 8D614F5D-019F-4C43-82A7-F29441FAEA6C localComputer diff --git a/FlatStore/FlatBatchUpdate.swift b/FlatStore/FlatBatchUpdate.swift index 7655e56..b4eb119 100644 --- a/FlatStore/FlatBatchUpdate.swift +++ b/FlatStore/FlatBatchUpdate.swift @@ -26,7 +26,8 @@ public final class FlatBatchUpdatesContext { // private let store: FlatStore - var buffer: [FlatStoreAnyIdentifier : Any] = [:] + var buffer = InMemoryEntityStorage() + private let store: FlatStore public init(store: FlatStore) { @@ -35,21 +36,29 @@ public final class FlatBatchUpdatesContext { public func set(value: T) -> FlatStoreObjectIdentifier { let key = value.id - buffer[key.asAny] = value + buffer.update(inTable: T.FlatStoreID.tableName) { (table) in + table.byID[key.raw] = value + } return key } - public func set(values: T) -> [FlatStoreObjectIdentifier] where T.Element : FlatStoreObjectType { - return - values.map { value -> FlatStoreObjectIdentifier in - let key = value.id - buffer[key.asAny] = value - return key + public func set(values: T) -> [FlatStoreObjectIdentifier] where T.Element : FlatStoreObjectType { + + var ids: [FlatStoreObjectIdentifier] = [] + + buffer.update(inTable: T.Element.FlatStoreID.tableName) { (table) in + values.forEach { value in + table.byID[value.id.raw] = value + ids.append(value.id) + } } + + return ids } public func get(by key: FlatStoreObjectIdentifier) -> T? { - if let transientObject = (buffer[key.asAny] as? T) { + + if let transientObject = (buffer.table(name: T.FlatStoreID.tableName)?.byID[key.raw] as? T) { return transientObject } if let object = store.get(by: key) { diff --git a/FlatStore/FlatStore.swift b/FlatStore/FlatStore.swift index b67d6a7..be8e957 100644 --- a/FlatStore/FlatStore.swift +++ b/FlatStore/FlatStore.swift @@ -23,14 +23,14 @@ import Foundation public struct FlatStoreAnyIdentifier : Hashable { - public let typeName: String - public let raw: AnyHashable + public let tableName: String + public let id: AnyHashable public let notificationName: Notification.Name - public init(typeName: String, rawID: AnyHashable) { - self.typeName = typeName - self.raw = rawID - self.notificationName = .init(rawValue: "\(typeName)|\(rawID)") + public init(tableName: String, id: AnyHashable) { + self.tableName = tableName + self.id = id + self.notificationName = .init(rawValue: "\(tableName)|\(id)") } } @@ -41,6 +41,10 @@ public struct FlatStoreObjectIdentifier : Hashable, Cus return raw } + public static var tableName: String { + typeName + } + public static var typeName: String { String.init(reflecting: T.self) } @@ -58,7 +62,10 @@ public struct FlatStoreObjectIdentifier : Hashable, Cus public init(_ raw: T.RawIDType) { self.raw = raw - self.asAny = FlatStoreAnyIdentifier(typeName: Self.typeName, rawID: .init(raw)) + self.asAny = FlatStoreAnyIdentifier( + tableName: Self.typeName, + id: .init(raw) + ) } public var description: String { @@ -92,7 +99,12 @@ public final class FlatStoreNotificationToken : NotificationTokenType { } } -public protocol FlatStoreObjectType { +public protocol _FlatStoreObjectType { + + var notificationName: Notification.Name { get } +} + +public protocol FlatStoreObjectType: _FlatStoreObjectType { associatedtype RawIDType : Hashable var rawID: RawIDType { get } @@ -111,35 +123,31 @@ extension FlatStoreObjectType { return FlatStoreObjectIdentifier.init(rawID) } + public var notificationName: Notification.Name { + id.notificationName + } + } -public protocol PersistentStoreType { - - typealias Storage = [FlatStoreAnyIdentifier : Any] - - var rawStorage: Storage { get } - var allItemCount: Int { get } - - func updateStorage(_ update: (inout Storage) -> ()) - +public struct EntityTable { + public var byID: [AnyHashable : Any] = [:] } -public final class InMemoryPersistentStore: PersistentStoreType { +public protocol EntityStorageType { - public var allItemCount: Int { - return rawStorage.count - } + typealias Storage = [AnyHashable : EntityTable] - public var rawStorage: Storage = [:] + var allItemCount: Int { get } - public init() { - - } + func allItems() -> [Any] - public func updateStorage(_ update: (inout Storage) -> ()) { - update(&rawStorage) - } + mutating func update(inTable name: String, update: (inout EntityTable) -> Void) + func table(name: String) -> EntityTable? + + mutating func removeAll() + + mutating func merge(inMemoryStorage: InMemoryEntityStorage) } // MARK: FlatStore @@ -150,7 +158,7 @@ open class FlatStore { storage.allItemCount } - private let storage: PersistentStoreType + private var storage: EntityStorageType private let notificationCenter: NotificationCenter = .init() @@ -160,7 +168,7 @@ open class FlatStore { private let lock = NSRecursiveLock() - public init(persistentStore: PersistentStoreType = InMemoryPersistentStore()) { + public init(persistentStore: EntityStorageType = InMemoryEntityStorage()) { self.storage = persistentStore notificationQueue.maxConcurrentOperationCount = 1 } @@ -177,33 +185,33 @@ extension FlatStore { public func get(by id: FlatStoreObjectIdentifier) -> T? { lock.lock(); defer { lock.unlock() } - return storage.rawStorage[id.asAny] as? T + return storage.table(name: T.FlatStoreID.tableName)?.byID[id.raw] as? T } public func get(by ids: S) -> [T] where S.Element == FlatStoreObjectIdentifier { lock.lock(); defer { lock.unlock() } return ids.compactMap { key in - storage.rawStorage[key.asAny] as? T + storage.table(name: T.FlatStoreID.tableName)?.byID[key.raw] as? T } } public func get(by id: FlatStoreAnyIdentifier) -> Any? { lock.lock(); defer { lock.unlock() } - return storage.rawStorage[id] + return storage.table(name: id.tableName)?.byID[id.id] } @discardableResult public func set(value: T) -> T.CachingRef { - let key = value.id + let id = value.id lock.lock() - storage.updateStorage { (store) in - _ = store[key.asAny] = value + storage.update(inTable: T.FlatStoreID.tableName) { (table) in + table.byID[id.raw] = value } lock.unlock() - let notification = makeSeparatedNotificationName(key.notificationName) + let notification = makeSeparatedNotificationName(id.notificationName) notificationQueue.addOperation { self.notificationCenter.post(name: notification, object: value) } @@ -226,21 +234,23 @@ extension FlatStore { let key = value.id lock.lock() - storage.updateStorage { (store) in - _ = store.removeValue(forKey: key.asAny) + storage.update(inTable: T.FlatStoreID.tableName) { (table) in + table.byID.removeValue(forKey: key.raw) } lock.unlock() - - let notification = makeSeparatedNotificationName(key.notificationName) - notificationQueue.addOperation { - self.notificationCenter.post(name: notification, object: value) - } + + dispatchUpdateNotification(name: key.notificationName, value: value) } public func deleteAll() { lock.lock(); defer { lock.unlock() } - storage.updateStorage { (store) in - _ = store.removeAll() + storage.removeAll() + } + + func dispatchUpdateNotification(name: Notification.Name, value: Any) { + let notification = makeSeparatedNotificationName(name) + notificationQueue.addOperation { + self.notificationCenter.post(name: notification, object: value) } } @@ -251,8 +261,7 @@ extension FlatStore { public func allObjects(type: T.Type) -> [T] { lock.lock(); defer { lock.unlock() } - let typeName = T.FlatStoreID.typeName - return storage.rawStorage.filter { $0.key.typeName == typeName }.map { $0.value } as! [T] + return storage.table(name: T.FlatStoreID.tableName)?.byID.map { $0.value } as! [T] } } @@ -389,12 +398,13 @@ extension FlatStore { let context = FlatBatchUpdatesContext(store: self) let u = try updates(self, context) - - storage.updateStorage { (store) in - for item in context.buffer { - store[item.key] = item.value - } + + storage.merge(inMemoryStorage: context.buffer) + + for item in context.buffer.allItems() as! [_FlatStoreObjectType] { + dispatchUpdateNotification(name: item.notificationName, value: item) } + return u } diff --git a/FlatStore/InMemoryEntityStorage.swift b/FlatStore/InMemoryEntityStorage.swift new file mode 100644 index 0000000..26915da --- /dev/null +++ b/FlatStore/InMemoryEntityStorage.swift @@ -0,0 +1,89 @@ +// +// InMemoryEntityStorage.swift +// FlatStore +// +// Created by muukii on 2019/11/05. +// Copyright © 2019 eure. All rights reserved. +// + +import Foundation + +public struct InMemoryEntityStorage: EntityStorageType { + + public var allItemCount: Int { + backingStore.reduce(0) { (count, arg1) -> Int in + let (_, value) = arg1 + return count + value.byID.count + } + } + + private var backingStore: Storage = [:] + private let _lock = NSRecursiveLock() + + public init() { + + } + + public mutating func update(inTable name: String, update: (inout EntityTable) -> Void) { + if backingStore[name] != nil { + update(&backingStore[name]!) + } else { + var table = EntityTable() + update(&table) + backingStore[name] = table + } + } + + public func table(name: String) -> EntityTable? { + backingStore[name] + } + + public mutating func removeAll() { + backingStore = [:] + } + + public func allItems() -> [Any] { + backingStore.reduce(into: [Any]()) { (r, arg1) in + let (_, value) = arg1 + let items = value.byID.map { $0.value } + r.append(contentsOf: items) + } + } + + public mutating func merge(inMemoryStorage: InMemoryEntityStorage) { + + inMemoryStorage.backingStore.forEach { key, value in + if var table = backingStore[key] { + var merged = table.byID + + value.byID.forEach { key, value in + merged[key] = value + } + + table.byID = merged + backingStore[key] = table + } else { + backingStore[key] = value + } + } + } + +} + +extension InMemoryEntityStorage { + + public struct Getter { + + let storage: InMemoryEntityStorage + + public init(storage: InMemoryEntityStorage) { + self.storage = storage + } + + public func find(by id: Entity.FlatStoreID) -> Entity? { + storage.table(name: Entity.FlatStoreID.tableName)?.byID[id.raw] as? Entity + } + + } + +} diff --git a/FlatStoreTests/FlatStoreTests.swift b/FlatStoreTests/FlatStoreTests.swift index e6e7f3a..da8f92e 100644 --- a/FlatStoreTests/FlatStoreTests.swift +++ b/FlatStoreTests/FlatStoreTests.swift @@ -162,6 +162,18 @@ class FlatStoreTests: XCTestCase { XCTAssertEqual(ref.value, comment) } + + func testNestedObject() { + + let user = User(name: "Hey") + let comment = Comment(rawID: "commen_1", userID: user.id) + + storeA.performBatchUpdates { (store, context) -> Void in + _ = context.set(value: user) + _ = context.set(value: comment) + } + + } func testNotificationDifferentStore() {