Skip to content

Commit e05173a

Browse files
stephencelismluisbrown
authored andcommitted
Update Store scoping internals to be more efficient (#1316)
* Add store scope * Reducer holds parent * wip * wip * wip * benchmark * wip * wip Co-authored-by: Pat Brown <[email protected]> Co-authored-by: Brandon Williams <[email protected]> (cherry picked from commit 15100ddb24bf5417136406da860b691de39e97ac) # Conflicts: # Sources/ComposableArchitecture/Store.swift
1 parent e4d62bf commit e05173a

File tree

2 files changed

+71
-24
lines changed

2 files changed

+71
-24
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public final class Store<State, Action> {
138138
private var isSending = false
139139
private let reducer: (inout State, Action) -> Effect<Action, Never>
140140
private var bufferedActions: [Action] = []
141+
fileprivate var scope: AnyScope?
141142
#if DEBUG
142143
private let mainThreadChecksEnabled: Bool
143144
#endif
@@ -307,29 +308,9 @@ public final class Store<State, Action> {
307308
action fromChildAction: @escaping (ChildAction) -> Action
308309
) -> Store<ChildState, ChildAction> {
309310
self.threadCheck(status: .scope)
310-
var isSending = false
311-
let childStore = Store<ChildState, ChildAction>(
312-
initialState: toChildState(self.state),
313-
reducer: .init { childState, childAction, _ in
314-
isSending = true
315-
defer { isSending = false }
316-
let task = self.send(fromChildAction(childAction))
317-
childState = toChildState(self.state)
318-
if let task = task {
319-
return .fireAndForget { await task.cancellableValue }
320-
} else {
321-
return .none
322-
}
323-
},
324-
environment: ()
325-
)
326-
childStore.parentDisposable = self.producer
327-
.skip(first: 1)
328-
.startWithValues { [weak childStore] newValue in
329-
guard !isSending else { return }
330-
childStore?.state = toChildState(newValue)
331-
}
332-
return childStore
311+
312+
return (self.scope ?? Scope(root: self))
313+
.rescope(self, state: toChildState, action: fromChildAction)
333314
}
334315

335316
/// Scopes the store to one that exposes child state.
@@ -566,3 +547,69 @@ public final class Store<State, Action> {
566547
#endif
567548
}
568549
}
550+
551+
private protocol AnyScope {
552+
func rescope<State, Action, ChildState, ChildAction>(
553+
_ store: Store<State, Action>,
554+
state toNewChildState: @escaping (State) -> ChildState,
555+
action fromNewChildAction: @escaping (ChildAction) -> Action
556+
) -> Store<ChildState, ChildAction>
557+
}
558+
559+
private struct Scope<RootState, RootAction>: AnyScope {
560+
let root: Store<RootState, RootAction>
561+
let toChildState: Any
562+
let fromChildAction: Any
563+
564+
init(root: Store<RootState, RootAction>) {
565+
self.init(root: root, toChildState: { $0 }, fromChildAction: { $0 })
566+
}
567+
568+
private init<State, Action>(
569+
root: Store<RootState, RootAction>,
570+
toChildState: @escaping (RootState) -> State,
571+
fromChildAction: @escaping (Action) -> RootAction
572+
) {
573+
self.root = root
574+
self.toChildState = toChildState
575+
self.fromChildAction = fromChildAction
576+
}
577+
578+
func rescope<State, Action, ChildState, ChildAction>(
579+
_ store: Store<State, Action>,
580+
state toChildState: @escaping (State) -> ChildState,
581+
action fromChildAction: @escaping (ChildAction) -> Action
582+
) -> Store<ChildState, ChildAction> {
583+
let toState = self.toChildState as! (RootState) -> State
584+
let fromAction = self.fromChildAction as! (Action) -> RootAction
585+
586+
var isSending = false
587+
let childStore = Store<ChildState, ChildAction>(
588+
initialState: toChildState(store.state),
589+
reducer: .init { childState, childAction, _ in
590+
isSending = true
591+
defer { isSending = false }
592+
let task = self.root.send(fromAction(fromChildAction(childAction)))
593+
childState = toChildState(store.state)
594+
if let task = task {
595+
return .fireAndForget { await task.cancellableValue }
596+
} else {
597+
return .none
598+
}
599+
},
600+
environment: ()
601+
)
602+
childStore.parentDisposable = store.producer
603+
.skip(first: 1)
604+
.startWithValues { [weak childStore] newValue in
605+
guard !isSending else { return }
606+
childStore?.state = toChildState(newValue)
607+
}
608+
childStore.scope = Scope<RootState, RootAction>(
609+
root: self.root,
610+
toChildState: { toChildState(toState($0)) },
611+
fromChildAction: { fromAction(fromChildAction($0)) }
612+
)
613+
return childStore
614+
}
615+
}

Tests/ComposableArchitectureTests/EffectOperationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class EffectOperationTests: XCTestCase {
110110
XCTFail()
111111
}
112112

113-
XCTAssertEqual(values, [42, 1729])
113+
XCTAssertEqual(values.sorted(), [42, 1729]) // merge is racy, hence the sorted()
114114
}
115115

116116
func testConcatenateFuses() async {

0 commit comments

Comments
 (0)