-
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
238 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
|
||
/** | ||
Against Derived, StoreMap won't retain the value from the store. | ||
sink function works with store directly. | ||
state property retrieves the value from state of the store by mapping. | ||
*/ | ||
public final class StoreMap<Store: StoreType, Mapped: Equatable>: Sendable { | ||
|
||
public typealias State = Store.State | ||
|
||
public let store: Store | ||
|
||
private let _map: @Sendable (Store.State) -> Mapped | ||
|
||
public var state: Changes<Mapped> { | ||
store.state.map(_map) | ||
} | ||
|
||
public init(store: Store, map: @escaping @Sendable (borrowing Store.State) -> Mapped) { | ||
self.store = store | ||
self._map = map | ||
} | ||
|
||
} | ||
|
||
// MARK: Implementations | ||
extension StoreMap { | ||
|
||
/** | ||
Start subscribing state updates in receive closure. | ||
It skips publishing values if the mapped value is not changed. | ||
*/ | ||
@_disfavoredOverload | ||
public func sinkState( | ||
dropsFirst: Bool = false, | ||
queue: MainActorTargetQueue = .mainIsolated(), | ||
receive: @escaping @MainActor (Changes<Mapped>) -> Void | ||
) -> StoreSubscription { | ||
|
||
let subscription = store.asStore() | ||
.sinkState( | ||
dropsFirst: dropsFirst, | ||
queue: queue, | ||
receive: { [_map] state in | ||
|
||
let mapped = state | ||
.map(_map) | ||
|
||
mapped.ifChanged().do { _ in | ||
receive(mapped) | ||
} | ||
|
||
} | ||
) | ||
|
||
_ = subscription.associate(object: self) | ||
|
||
return subscription | ||
} | ||
|
||
/** | ||
Start subscribing state updates in receive closure. | ||
It skips publishing values if the mapped value is not changed. | ||
*/ | ||
public func sinkState( | ||
dropsFirst: Bool = false, | ||
queue: some TargetQueueType, | ||
receive: @escaping (Changes<Mapped>) -> Void | ||
) -> StoreSubscription { | ||
|
||
let subscription = store.asStore() | ||
.sinkState( | ||
dropsFirst: dropsFirst, | ||
queue: queue, | ||
receive: { [_map] state in | ||
|
||
let mapped = state | ||
.map(_map) | ||
|
||
mapped.ifChanged().do { | ||
receive(mapped) | ||
} | ||
|
||
} | ||
) | ||
|
||
_ = subscription.associate(object: self) | ||
|
||
return subscription | ||
} | ||
|
||
/** | ||
Assigns a Store's state to a property of a store. | ||
|
||
- Returns: a cancellable. See detail of handling cancellable from ``StoreSubscription``'s docs | ||
*/ | ||
public func assign( | ||
queue: some TargetQueueType = .passthrough, | ||
to binder: @escaping (Changes<State>) -> Void | ||
) -> StoreSubscription { | ||
store.asStore().sinkState(queue: queue, receive: binder) | ||
} | ||
|
||
/** | ||
Assigns a Store's state to a property of a store. | ||
|
||
- Returns: a cancellable. See detail of handling cancellable from ``StoreSubscription``'s docs | ||
*/ | ||
public func assign( | ||
queue: MainActorTargetQueue, | ||
to binder: @escaping (Changes<State>) -> Void | ||
) -> StoreSubscription { | ||
store.asStore().sinkState(queue: queue, receive: binder) | ||
} | ||
|
||
} | ||
|
||
extension StoreType { | ||
|
||
public func map<Mapped>(_ map: @escaping @Sendable (State) -> Mapped) -> StoreMap<Self, Mapped> { | ||
.init(store: self, map: map) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import Verge | ||
import XCTest | ||
|
||
final class StoreAndDerivedTests: XCTestCase { | ||
|
||
@MainActor | ||
func test() async { | ||
|
||
let store = Store<_, Never>(initialState: DemoState()) | ||
|
||
let nameDerived = store.derived(.select(\.name)) | ||
let countDerived = store.derived(.select(\.count)) | ||
|
||
let countMap = store.map(\.count) | ||
|
||
await withTaskGroup(of: Void.self) { group in | ||
|
||
for i in 0..<1000 { | ||
group.addTask { | ||
await withBackground { | ||
store.commit { | ||
$0.name = "\(i)" | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
group.addTask { | ||
await withBackground { | ||
store.commit { | ||
$0.count = 100 | ||
} | ||
} | ||
|
||
XCTAssertEqual(store.state.count, 100) | ||
XCTAssertEqual(countMap.state.primitive, 100) | ||
|
||
// potentially it fails as EventEmitter's behavior | ||
// If EventEmitter's buffer is not empty, commit function escape from the stack by only adding. | ||
// XCTAssertEqual(countDerived.state.primitive, 100) | ||
XCTAssertNotEqual(countDerived.state.primitive, 100) | ||
} | ||
|
||
} | ||
|
||
print("end") | ||
|
||
} | ||
|
||
} | ||
|
||
/** | ||
Performs the given task in background | ||
*/ | ||
public nonisolated func withBackground<Return: Sendable>( | ||
_ thunk: @escaping @Sendable () async throws -> Return | ||
) async rethrows -> Return { | ||
|
||
// for now we will keep this until Swift6. | ||
assert(Thread.isMainThread == false) | ||
|
||
// here is the background as it's nonisolated | ||
// to inherit current actor context, use @_unsafeInheritExecutor | ||
|
||
// thunk closure runs on the background as it's sendable | ||
// if it's not sendable, inherit current actor context but it's already background. | ||
// @_inheritActorContext makes closure runs on current actor context even if it's sendable. | ||
return try await thunk() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Verge | ||
import XCTest | ||
|
||
final class StoreMapTests: XCTestCase { | ||
|
||
@MainActor | ||
func testBasic() { | ||
|
||
let store = Store<DemoState, Never>(initialState: .init()) | ||
|
||
let countState = store.map(\.count) | ||
|
||
var exp = expectation(description: "") | ||
exp.expectedFulfillmentCount = 2 | ||
|
||
let s = countState.sinkState { state in | ||
exp.fulfill() | ||
} | ||
|
||
// cause update | ||
store.commit { | ||
$0.count += 1 | ||
} | ||
|
||
// won't cause update | ||
store.commit { | ||
$0.count = 1 | ||
} | ||
|
||
wait(for: [exp]) | ||
|
||
withExtendedLifetime(s, {}) | ||
|
||
} | ||
|
||
} |