-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathStore.swift
238 lines (212 loc) · 8.78 KB
/
Store.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import Combine
import Foundation
/// A store represents the runtime that powers the application. It is the object that you will pass
/// around to views that need to interact with the application.
///
/// You will typically construct a single one of these at the root of your application, and then use
/// the `scope` method to derive more focused stores that can be passed to subviews.
public final class Store<State, Action> {
var state: CurrentValueSubject<State, Never>
var effectCancellables: [UUID: AnyCancellable] = [:]
private var isSending = false
private var parentCancellable: AnyCancellable?
private let reducer: (inout State, Action) -> Effect<Action, Never>
private var synchronousActionsToSend: [Action] = []
/// Initializes a store from an initial state, a reducer, and an environment.
///
/// - Parameters:
/// - initialState: The state to start the application in.
/// - reducer: The reducer that powers the business logic of the application.
/// - environment: The environment of dependencies for the application.
public convenience init<Environment>(
initialState: State,
reducer: Reducer<State, Action, Environment>,
environment: Environment
) {
self.init(
initialState: initialState,
reducer: { reducer.run(&$0, $1, environment) }
)
}
/// Scopes the store to one that exposes local state and actions.
///
/// This can be useful for deriving new stores to hand to child views in an application. For
/// example:
///
/// // Application state made from local states.
/// struct AppState { var login: LoginState, ... }
/// struct AppAction { case login(LoginAction), ... }
///
/// // A store that runs the entire application.
/// let store = Store(initialState: AppState(), reducer: appReducer, environment: ())
///
/// // Construct a login view by scoping the store to one that works with only login domain.
/// let loginView = LoginView(
/// store: store.scope(
/// state: { $0.login },
/// action: { AppAction.login($0) }
/// )
/// )
///
/// - Parameters:
/// - toLocalState: A function that transforms `State` into `LocalState`.
/// - fromLocalAction: A function that transforms `LocalAction` into `Action`.
/// - Returns: A new store with its domain (state and action) transformed.
public func scope<LocalState, LocalAction>(
state toLocalState: @escaping (State) -> LocalState,
action fromLocalAction: @escaping (LocalAction) -> Action
) -> Store<LocalState, LocalAction> {
let localStore = Store<LocalState, LocalAction>(
initialState: toLocalState(self.state.value),
reducer: { localState, localAction in
self.send(fromLocalAction(localAction))
localState = toLocalState(self.state.value)
return .none
}
)
localStore.parentCancellable = self.state
.sink { [weak localStore] newValue in localStore?.state.value = toLocalState(newValue) }
return localStore
}
/// Scopes the store to one that exposes local state.
///
/// - Parameter toLocalState: A function that transforms `State` into `LocalState`.
/// - Returns: A new store with its domain (state and action) transformed.
public func scope<LocalState>(
state toLocalState: @escaping (State) -> LocalState
) -> Store<LocalState, Action> {
self.scope(state: toLocalState, action: { $0 })
}
/// Scopes the store to a publisher of stores of more local state and local actions.
///
/// - Parameters:
/// - toLocalState: A function that transforms a publisher of `State` into a publisher of
/// `LocalState`.
/// - fromLocalAction: A function that transforms `LocalAction` into `Action`.
/// - Returns: A publisher of stores with its domain (state and action) transformed.
public func scope<P: Publisher, LocalState, LocalAction>(
state toLocalState: @escaping (AnyPublisher<State, Never>) -> P,
action fromLocalAction: @escaping (LocalAction) -> Action
) -> AnyPublisher<Store<LocalState, LocalAction>, Never>
where P.Output == LocalState, P.Failure == Never {
func extractLocalState(_ state: State) -> LocalState? {
var localState: LocalState?
_ = toLocalState(Just(state).eraseToAnyPublisher())
.sink { localState = $0 }
return localState
}
return toLocalState(self.state.eraseToAnyPublisher())
.map { localState in
let localStore = Store<LocalState, LocalAction>(
initialState: localState,
reducer: { localState, localAction in
self.send(fromLocalAction(localAction))
localState = extractLocalState(self.state.value) ?? localState
return .none
})
localStore.parentCancellable = self.state
.sink { [weak localStore] state in
guard let localStore = localStore else { return }
localStore.state.value = extractLocalState(state) ?? localStore.state.value
}
return localStore
}
.eraseToAnyPublisher()
}
/// Scopes the store to a publisher of stores of more local state and local actions.
///
/// - Parameter toLocalState: A function that transforms a publisher of `State` into a publisher
/// of `LocalState`.
/// - Returns: A publisher of stores with its domain (state and action)
/// transformed.
public func scope<P: Publisher, LocalState>(
state toLocalState: @escaping (AnyPublisher<State, Never>) -> P
) -> AnyPublisher<Store<LocalState, Action>, Never>
where P.Output == LocalState, P.Failure == Never {
self.scope(state: toLocalState, action: { $0 })
}
func send(_ action: Action) {
self.synchronousActionsToSend.append(action)
while !self.synchronousActionsToSend.isEmpty {
let action = self.synchronousActionsToSend.removeFirst()
if self.isSending {
assertionFailure(
"""
The store was sent the action \(debugCaseOutput(action)) while it was already
processing another action.
This can happen for a few reasons:
* The store was sent an action recursively. This can occur when you run an effect \
directly in the reducer, rather than returning it from the reducer. Check the stack (⌘7) \
to find frames corresponding to one of your reducers. That code should be refactored to \
not invoke the effect directly.
* The store has been sent actions from multiple threads. The `send` method is not \
thread-safe, and should only ever be used from a single thread (typically the main \
thread). Instead of calling `send` from multiple threads you should use effects to \
process expensive computations on background threads so that it can be fed back into the \
store.
"""
)
}
self.isSending = true
let effect = self.reducer(&self.state.value, action)
self.isSending = false
var didComplete = false
let uuid = UUID()
var isProcessingEffects = true
let effectCancellable = effect.sink(
receiveCompletion: { [weak self] _ in
didComplete = true
self?.effectCancellables[uuid] = nil
},
receiveValue: { [weak self] action in
if isProcessingEffects {
self?.synchronousActionsToSend.append(action)
} else {
self?.send(action)
}
}
)
isProcessingEffects = false
if !didComplete {
self.effectCancellables[uuid] = effectCancellable
}
}
}
/// Returns a "stateless" store by erasing state to `Void`.
public var stateless: Store<Void, Action> {
self.scope(state: { _ in () })
}
/// Returns an "actionless" store by erasing action to `Never`.
public var actionless: Store<State, Never> {
func absurd<A>(_ never: Never) -> A {}
return self.scope(state: { $0 }, action: absurd)
}
private init(
initialState: State,
reducer: @escaping (inout State, Action) -> Effect<Action, Never>
) {
self.reducer = reducer
self.state = CurrentValueSubject(initialState)
}
}
/// A publisher of store state.
@dynamicMemberLookup
public struct StorePublisher<State>: Publisher {
public typealias Output = State
public typealias Failure = Never
public let upstream: AnyPublisher<State, Never>
public func receive<S>(subscriber: S)
where S: Subscriber, Failure == S.Failure, Output == S.Input {
self.upstream.subscribe(subscriber)
}
init<P>(_ upstream: P) where P: Publisher, Failure == P.Failure, Output == P.Output {
self.upstream = upstream.eraseToAnyPublisher()
}
/// Returns the resulting publisher of a given key path.
public subscript<LocalState>(
dynamicMember keyPath: KeyPath<State, LocalState>
) -> StorePublisher<LocalState>
where LocalState: Equatable {
.init(self.upstream.map(keyPath).removeDuplicates())
}
}