Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Numerous Fixes For Chained Operations And Performance Improvements #66

Merged
merged 6 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ let package = Package(
exclude: [
"../../Images",
"../../Performance Profiler",
],
swiftSettings: [
.define("ENABLE_TESTABILITY", .when(configuration: .debug))
]
),
.testTarget(
Expand Down
18 changes: 18 additions & 0 deletions Sources/Boutique/SecurelyStoredValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ public struct SecurelyStoredValue<Item: Codable> {
public func remove() throws {
if self.wrappedValue != nil {
try self.removeItem()
} else if self.wrappedValue == nil && Self.keychainValueExists(group: self.group, service: self.keychainService, account: self.key) {
try self.removeItem()
}
}

Expand Down Expand Up @@ -234,6 +236,22 @@ private extension SecurelyStoredValue {
}
}

static func keychainValueExists(group: String?, service: String, account: String) -> Bool {
let keychainQuery = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecReturnData: true
]
.withGroup(group)
.mapToStringDictionary()

var extractedData: AnyObject?
let status = SecItemCopyMatching(keychainQuery as CFDictionary, &extractedData)

return status != errSecItemNotFound
}

var keychainService: String {
self.service ?? Self.defaultService
}
Expand Down
37 changes: 23 additions & 14 deletions Sources/Boutique/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,14 +385,16 @@ public extension Store {
// Internal versions of the `insert`, `remove`, and `removeAll` function code paths so we can avoid duplicating code.
internal extension Store {
func performInsert(_ item: Item, firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
var currentItems = await self.items

if let strategy = existingItemsStrategy {
// Remove items from disk and memory based on the cache invalidation strategy
var removedItems: [Item] = [item]
try await self.removeItems(&removedItems, withStrategy: strategy)
var removedItems = [item]
try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy)
// If we remove this one it will error
self.removeItemsFromMemory(&currentItems, withStrategy: strategy, identifier: cacheIdentifier)
}

// Take the current items array and turn it into an OrderedDictionary.
let currentItems = await self.items
let identifier = item[keyPath: self.cacheIdentifier]
let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] })
var currentValuesDictionary = OrderedDictionary<String, Item>(uniqueKeys: currentItemsKeys, values: currentItems)
Expand All @@ -407,11 +409,16 @@ internal extension Store {
}

func performInsert(_ items: [Item], firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
var currentItems = await self.items

if let strategy = existingItemsStrategy {
// Remove items from disk and memory based on the cache invalidation strategy
var removedItems = items
try await self.removeItems(&removedItems, withStrategy: strategy)
try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy)
// This one is fine to remove... but why?
// Is it the way we construct the items in the ordered dictionary?
// If so should the two just use the same approach — perhaps sharing all the same code except for the last call to `persistItem` vs. `persistItems`?
self.removeItemsFromMemory(&currentItems, withStrategy: strategy, identifier: cacheIdentifier)
}

var insertedItemsDictionary = OrderedDictionary<String, Item>()
Expand All @@ -424,7 +431,6 @@ internal extension Store {
}

// Take the current items array and turn it into an OrderedDictionary.
let currentItems = await self.items
let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] })
var currentValuesDictionary = OrderedDictionary<String, Item>(uniqueKeys: currentItemsKeys, values: currentItems)

Expand Down Expand Up @@ -477,7 +483,6 @@ internal extension Store {
}

private extension Store {

func persistItem(_ item: Item) async throws {
let cacheKey = CacheKey(item[keyPath: self.cacheIdentifier])

Expand All @@ -503,18 +508,12 @@ private extension Store {
try await self.storageEngine.remove(keys: itemKeys)
}

func removeItems(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy<Item>) async throws {
func removeItemsFromStorageEngine(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy<Item>) async throws {
let itemsToRemove = strategy.removedItems(items)

// If we're using the `.removeNone` strategy then there are no items to invalidate and we can return early
guard itemsToRemove.count != 0 else { return }

items = items.filter { item in
!itemsToRemove.contains(where: {
$0[keyPath: cacheIdentifier] == item[keyPath: cacheIdentifier]
}
)}

let itemKeys = itemsToRemove.map({ CacheKey(verbatim: $0[keyPath: self.cacheIdentifier]) })

if itemKeys.count == 1 {
Expand All @@ -523,4 +522,14 @@ private extension Store {
try await self.storageEngine.remove(keys: itemKeys)
}
}

func removeItemsFromMemory(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy<Item>, identifier: KeyPath<Item, String>) {
let itemsToRemove = strategy.removedItems(items)

items = items.filter { item in
!itemsToRemove.contains(where: {
$0[keyPath: identifier] == item[keyPath: identifier]
}
)}
}
}
Loading
Loading