Skip to content

Commit

Permalink
V4.0.0 (#25)
Browse files Browse the repository at this point in the history
* Add breaking changes for 4.0.0

* Improve logging and test benchmarks for issues

* Test removing main from didChange

* Update c to bug/ARC-issue branch

* Protect setting effect task with lock

* Add weak for scoped Store

* Use errors from c

* Resolve memory issue

* Update c to 3.0 and remove debug tests

* Uncomment temp removed code

* Add back main queue receive on

* Update StoreView to improve performance

* Switch Binding param order

* Update README

* Add StoreContentTests
  • Loading branch information
0xLeif authored Oct 26, 2022
1 parent 75774cd commit aa4e884
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 101 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
// Dependencies declare other packages that this package depends on.
.package(
url: "https://github.com/0xOpenBytes/c",
from: "1.1.1"
from: "3.0.0"
),
.package(
url: "https://github.com/0xLeif/swift-custom-dump",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ let actionHandler = StoreActionHandler<StoreKey, Action, Dependency> { cacheStor
struct ContentView: View {
@ObservedObject var store: Store<StoreKey, Action, Dependency> = .init(
initialValues: [
.url: URL(string: "https://jsonplaceholder.typicode.com/posts") as Any
.url: URL(string: "https://jsonplaceholder.typicode.com/posts")!
],
actionHandler: actionHandler,
dependency: .live
Expand Down
40 changes: 19 additions & 21 deletions Sources/CacheStore/Stores/CacheStore/CacheStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,14 @@ import SwiftUI
/// An `ObservableObject` that has a `cache` which is the source of truth for this object
open class CacheStore<Key: Hashable>: ObservableObject, Cacheable {
/// `Error` that reports the missing keys for the `CacheStore`
public struct MissingRequiredKeysError<Key: Hashable>: LocalizedError {
/// Required keys
public let keys: Set<Key>

/// init for `MissingRequiredKeysError<Key>`
public init(keys: Set<Key>) {
self.keys = keys
}

/// Error description for `LocalizedError`
public var errorDescription: String? {
"Missing Required Keys: \(keys.map { "\($0)" }.joined(separator: ", "))"
}
}
public typealias MissingRequiredKeysError = c.MissingRequiredKeysError

/// `Error` that reports the expected type for a value in the `CacheStore`
public typealias InvalidTypeError = c.InvalidTypeError

private var lock: NSLock
@Published var cache: [Key: Any]

/// The values in the `cache` of type `Any`
public var valuesInCache: [Key: Any] { cache }


/// init for `CacheStore<Key>`
required public init(initialValues: [Key: Any]) {
lock = NSLock()
Expand Down Expand Up @@ -63,7 +50,17 @@ open class CacheStore<Key: Hashable>: ObservableObject, Cacheable {
}

/// Resolve the `Value` for the `Key` by force casting `get`
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) -> Value { get(key)! }
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) throws -> Value {
guard contains(key) else {
throw MissingRequiredKeysError(keys: [key])
}

guard let value: Value = get(key) else {
throw InvalidTypeError(expectedType: Value.self, actualValue: get(key))
}

return value
}

/// Set the `Value` for the `Key`
public func set<Value>(value: Value, forKey key: Key) {
Expand Down Expand Up @@ -190,10 +187,11 @@ public extension CacheStore {
/// Creates a `Binding` for the given `Key`
func binding<Value>(
_ key: Key,
as: Value.Type = Value.self
as: Value.Type = Value.self,
fallback: Value
) -> Binding<Value> {
Binding(
get: { self.resolve(key) },
get: { self.get(key) ?? fallback },
set: { self.set(value: $0, forKey: key) }
)
}
Expand Down
17 changes: 12 additions & 5 deletions Sources/CacheStore/Stores/Store/Content/StoreView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@ public protocol StoreView: View {
associatedtype Dependency
/// The content the View cares about and uses
associatedtype Content: StoreContent

/// The View created from the current Content
associatedtype ContentView: View

/// An `ObservableObject` that uses actions to modify the state which is a `CacheStore`
var store: Store<Key, Action, Dependency> { get set }
/// The content a StoreView uses when creating SwiftUI views
var content: Content { get }


init(store: Store<Key, Action, Dependency>)

/// Create the body view with the current Content of the Store. View's body property is defaulted to using this function.
/// - Parameters:
/// - content: The content a StoreView uses when creating SwiftUI views
func body(content: Content) -> ContentView
}

public extension StoreView where Content.Key == Key {
var content: Content { store.content() }
var body: some View {
body(content: store.content())
}
}
69 changes: 44 additions & 25 deletions Sources/CacheStore/Stores/Store/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
private var isDebugging: Bool
private var cacheStoreObserver: AnyCancellable?
private var effects: [AnyHashable: Task<(), Never>]

var cacheStore: CacheStore<Key>
var actionHandler: StoreActionHandler<Key, Action, Dependency>
let dependency: Dependency
private(set) var cacheStore: CacheStore<Key>
private(set) var actionHandler: StoreActionHandler<Key, Action, Dependency>
private let dependency: Dependency

/// The values in the `cache` of type `Any`
public var valuesInCache: [Key: Any] {
public var allValues: [Key: Any] {
lock.lock()
defer { lock.unlock() }

return cacheStore.valuesInCache
return cacheStore.allValues
}

/// A publisher for the private `cache` that is mapped to a CacheStore
Expand All @@ -32,7 +31,7 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
}

/// An identifier of the Store and CacheStore
var debugIdentifier: String {
public var debugIdentifier: String {
lock.lock()
defer { lock.unlock() }

Expand Down Expand Up @@ -66,11 +65,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
cacheStore = CacheStore(initialValues: initialValues)
self.actionHandler = actionHandler
self.dependency = dependency
cacheStoreObserver = publisher.sink { [weak self] _ in
DispatchQueue.main.async {
cacheStoreObserver = cacheStore.$cache
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}

/// Get the value in the `cache` using the `key`. This returns an optional value. If the value is `nil`, that means either the value doesn't exist or the value is not able to be casted as `Value`.
Expand All @@ -82,11 +81,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
}

/// Resolve the value in the `cache` using the `key`. This function uses `get` and force casts the value. This should only be used when you know the value is always in the `cache`.
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) -> Value {
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) throws -> Value {
lock.lock()
defer { lock.unlock() }

return cacheStore.resolve(key)
return try cacheStore.resolve(key)
}

/// Checks to make sure the cache has the required keys, otherwise it will throw an error
Expand Down Expand Up @@ -125,6 +124,9 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan

/// Cancel an effect with the ID
public func cancel(id: AnyHashable) {
lock.lock()
defer { lock.unlock() }

effects[id]?.cancel()
effects[id] = nil
}
Expand Down Expand Up @@ -171,11 +173,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan

scopedStore.cacheStore = scopedCacheStore
scopedStore.parentStore = self
scopedStore.actionHandler = StoreActionHandler { (store: inout CacheStore<ScopedKey>, action: ScopedAction, dependency: ScopedDependency) in
scopedStore.actionHandler = StoreActionHandler { [weak scopedStore] (store: inout CacheStore<ScopedKey>, action: ScopedAction, dependency: ScopedDependency) in
let effect = actionHandler.handle(store: &store, action: action, dependency: dependency)

if let parentAction = actionTransformation(action) {
scopedStore.parentStore?.handle(action: parentAction)
scopedStore?.parentStore?.handle(action: parentAction)
}

return effect
Expand Down Expand Up @@ -208,11 +210,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
/// Creates a `Binding` for the given `Key` using an `Action` to set the value
public func binding<Value>(
_ key: Key,
as: Value.Type = Value.self,
fallback: Value,
using: @escaping (Value) -> Action
) -> Binding<Value> {
Binding(
get: { self.resolve(key) },
get: { self.get(key) ?? fallback },
set: { self.handle(action: using($0)) }
)
}
Expand All @@ -233,6 +235,18 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
// MARK: - Void Dependency

public extension Store where Dependency == Void {
/// init for `Store<Key, Action, Void>`
convenience init(
initialValues: [Key: Any],
actionHandler: StoreActionHandler<Key, Action, Dependency>
) {
self.init(
initialValues: initialValues,
actionHandler: actionHandler,
dependency: ()
)
}

/// Creates a `ScopedStore`
func scope<ScopedKey: Hashable, ScopedAction>(
keyTransformation: BiDirectionalTransformation<Key?, ScopedKey?>,
Expand Down Expand Up @@ -285,15 +299,20 @@ extension Store {

if let actionEffect = actionEffect {
cancel(id: actionEffect.id)
effects[actionEffect.id] = Task {
let effectTask = Task { [weak self] in
defer { self?.cancel(id: actionEffect.id) }

if Task.isCancelled { return }

guard let nextAction = await actionEffect.effect() else { return }

if Task.isCancelled { return }
handle(action: nextAction)

self?.handle(action: nextAction)
}
lock.lock()
effects[actionEffect.id] = effectTask
lock.unlock()
}

if isDebugging {
Expand All @@ -303,7 +322,7 @@ extension Store {
--------------- State Output ------------
"""
)

if cacheStore.isCacheEqual(to: cacheStoreCopy) {
print("\t🙅 No State Change")
} else {
Expand All @@ -329,15 +348,15 @@ extension Store {
)
}
}

print(
"""
--------------- State End ---------------
[\(formattedDate)] 🏁 End Action: \(customDump(action)) \(debugIdentifier)
"""
)
}

cacheStore.cache = cacheStoreCopy.cache

return actionEffect
Expand All @@ -359,7 +378,7 @@ extension Store {

var updatedStateChanges: [String] = []

for (key, value) in updatedStore.valuesInCache {
for (key, value) in updatedStore.allValues {
let isValueEqual: Bool = cacheStore.isValueEqual(toUpdatedValue: value, forKey: key)
let valueInfo: String = "\(type(of: value))"
let valueOutput: String
Expand Down
24 changes: 15 additions & 9 deletions Sources/CacheStore/Stores/Store/TestStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ open class TestStore<Key: Hashable, Action, Dependency> {
file: StaticString = #filePath,
line: UInt = #line
) {
self.store = store.debug
self.store = store
effects = []
initFile = file
initLine = line
Expand All @@ -47,11 +47,18 @@ open class TestStore<Key: Hashable, Action, Dependency> {
file: StaticString = #filePath,
line: UInt = #line
) {
store = Store(initialValues: initialValues, actionHandler: actionHandler, dependency: dependency).debug
store = Store(initialValues: initialValues, actionHandler: actionHandler, dependency: dependency)
effects = []
initFile = file
initLine = line
}

/// Modifies and returns the `TestStore` with debugging mode on
public var debug: Self {
_ = store.debug

return self
}

/// Send an action and provide an expectation for the changes from handling the action
public func send(
Expand All @@ -61,7 +68,7 @@ open class TestStore<Key: Hashable, Action, Dependency> {
expecting: (inout CacheStore<Key>) throws -> Void
) {
var expectedCacheStore = store.cacheStore.copy()

let actionEffect = store.send(action)

do {
Expand All @@ -70,32 +77,31 @@ open class TestStore<Key: Hashable, Action, Dependency> {
TestStoreFailure.handler("❌ Expectation failed", file, line)
return
}

guard expectedCacheStore.isCacheEqual(to: store.cacheStore) else {
TestStoreFailure.handler(
"""
❌ Expectation failed
--- Expected ---
\(customDump(expectedCacheStore.valuesInCache))
\(customDump(expectedCacheStore.allValues))
----------------
****************
---- Actual ----
\(customDump(store.cacheStore.valuesInCache))
\(customDump(store.cacheStore.allValues))
----------------
""",
file,
line
)
return
}



if let actionEffect = actionEffect {
let predicate: (ActionEffect<Action>) -> Bool = { $0.id == actionEffect.id }
if effects.contains(where: predicate) {
effects.removeAll(where: predicate)
}

effects.append(actionEffect)
}
}
Expand Down
Loading

0 comments on commit aa4e884

Please sign in to comment.