@@ -18,12 +18,19 @@ import Result
1818///
1919/// `Action` enforces serial execution, and disables the `Action` during the execution.
2020public final class Action < Input, Output, Error: Swift . Error > {
21- private let deinitToken : Lifetime . Token
21+ private enum Availability {
22+ case enabledIdle
23+ case enabledExecuting
24+ case disabledIdle
25+ case disabledExecuting
26+ }
2227
23- private let executeClosure : ( _ state : Any , _ input : Input ) -> SignalProducer < Output , Error >
28+ private let execute : ( Action < Input , Output , Error > , Input ) -> SignalProducer < Output , ActionError < Error > >
2429 private let eventsObserver : Signal < Signal < Output , Error > . Event , NoError > . Observer
2530 private let disabledErrorsObserver : Signal < ( ) , NoError > . Observer
2631
32+ private let deinitToken : Lifetime . Token
33+
2734 /// The lifetime of the `Action`.
2835 public let lifetime : Lifetime
2936
@@ -37,13 +44,17 @@ public final class Action<Input, Output, Error: Swift.Error> {
3744 ///
3845 /// In other words, this sends every value from every unit of work that the `Action`
3946 /// executes.
40- public let values : Signal < Output , NoError >
47+ public private( set) lazy var values : Signal < Output , NoError > = { [ unowned self] in
48+ return self . events. filterMap { $0. value }
49+ } ( )
4150
4251 /// A signal of all errors generated from all units of work of the `Action`.
4352 ///
4453 /// In other words, this sends every error from every unit of work that the `Action`
4554 /// executes.
46- public let errors : Signal < Error , NoError >
55+ public private( set) lazy var errors : Signal < Error , NoError > = { [ unowned self] in
56+ return self . events. filterMap { $0. error }
57+ } ( )
4758
4859 /// A signal of all failed attempts to start a unit of work of the `Action`.
4960 public let disabledErrors : Signal < ( ) , NoError >
@@ -52,16 +63,16 @@ public final class Action<Input, Output, Error: Swift.Error> {
5263 ///
5364 /// In other words, this will send completed events from every signal generated
5465 /// by each SignalProducer returned from apply().
55- public let completed : Signal < ( ) , NoError >
66+ public private( set) lazy var completed : Signal < ( ) , NoError > = { [ unowned self] in
67+ return self . events. filter { $0. isCompleted } . map { _ in }
68+ } ( )
5669
5770 /// Whether the action is currently executing.
5871 public let isExecuting : Property < Bool >
5972
6073 /// Whether the action is currently enabled.
6174 public let isEnabled : Property < Bool >
6275
63- private let state : MutableProperty < ActionState >
64-
6576 /// Initializes an `Action` that would be conditionally enabled depending on its
6677 /// state.
6778 ///
@@ -80,39 +91,110 @@ public final class Action<Input, Output, Error: Swift.Error> {
8091 ///
8192 /// - parameters:
8293 /// - state: A property to be the state of the `Action`.
83- /// - enabledIf : A predicate which determines the availability of the `Action`,
94+ /// - isEnabled : A predicate which determines the availability of the `Action`,
8495 /// given the latest `Action` state.
8596 /// - execute: A closure that produces a unit of work, as `SignalProducer`, to be
8697 /// executed by the `Action`.
87- public init < State: PropertyProtocol > ( state property: State , enabledIf isEnabled: @escaping ( State . Value ) -> Bool , execute: @escaping ( State . Value , Input ) -> SignalProducer < Output , Error > ) {
98+ public init < State: PropertyProtocol > ( state: State , enabledIf isEnabled: @escaping ( State . Value ) -> Bool , execute: @escaping ( State . Value , Input ) -> SignalProducer < Output , Error > ) {
99+ typealias ActionState = ( availability: Availability , value: State . Value )
100+ let isUserEnabled = isEnabled
101+
88102 deinitToken = Lifetime . Token ( )
89103 lifetime = Lifetime ( deinitToken)
90-
91- // Retain the `property` for the created `Action`.
92- lifetime. observeEnded { _ = property }
93104
94- executeClosure = { state, input in execute ( state as! State . Value , input) }
105+ // `isExecuting` and `isEnabled` uses their own locks, so that legitimate
106+ // feedbacks would not deadlock.
107+ let initial = ( availability: Availability . enabledIdle, value: state. value)
108+ let actionState = MutableProperty < ActionState > ( initial)
109+ let isExecuting = MutableProperty ( false )
110+ let isEnabled = MutableProperty ( true )
111+ self . isExecuting = Property ( capturing: isExecuting)
112+ self . isEnabled = Property ( capturing: isEnabled)
113+
114+ // Associate the state property with the created `Action`.
115+ lifetime. observeEnded { _ = state }
95116
96117 ( events, eventsObserver) = Signal < Signal < Output , Error > . Event , NoError > . pipe ( )
97118 ( disabledErrors, disabledErrorsObserver) = Signal < ( ) , NoError > . pipe ( )
98119
99- values = events. filterMap { $0. value }
100- errors = events. filterMap { $0. error }
101- completed = events. filter { $0. isCompleted } . map { _ in }
120+ let disposable = state. producer. startWithValues { value in
121+ actionState. modify { state in
122+ state. value = value
123+
124+ if isUserEnabled ( value) {
125+ switch state. availability {
126+ case . enabledIdle, . enabledExecuting:
127+ break
128+
129+ case . disabledIdle:
130+ state. availability = . enabledIdle
131+ isEnabled. value = true
102132
103- let initial = ActionState ( value: property. value, isEnabled: { isEnabled ( $0 as! State . Value ) } )
104- state = MutableProperty ( initial)
133+ case . disabledExecuting:
134+ state. availability = . enabledExecuting
135+ }
136+ } else {
137+ switch state. availability {
138+ case . disabledIdle, . disabledExecuting:
139+ break
140+
141+ case . enabledIdle:
142+ state. availability = . disabledIdle
143+ isEnabled. value = false
105144
106- property. signal
107- . take ( during: state. lifetime)
108- . observeValues { [ weak state] newValue in
109- state? . modify {
110- $0. value = newValue
145+ case . enabledExecuting:
146+ state. availability = . disabledExecuting
147+ }
111148 }
112149 }
150+ }
113151
114- self . isEnabled = state. map { $0. isEnabled } . skipRepeats ( )
115- self . isExecuting = state. map { $0. isExecuting } . skipRepeats ( )
152+ lifetime. observeEnded ( disposable. dispose)
153+
154+ self . execute = { action, input in
155+ return SignalProducer { observer, disposable in
156+ func tryStart( ) -> State . Value ? {
157+ return actionState. modify { state in
158+ guard state. availability == . enabledIdle else {
159+ return nil
160+ }
161+
162+ state. availability = . enabledExecuting
163+ isExecuting. value = true
164+ isEnabled. value = false
165+ return state. value
166+ }
167+ }
168+
169+ guard let state = tryStart ( ) else {
170+ observer. send ( error: . disabled)
171+ action. disabledErrorsObserver. send ( value: ( ) )
172+ return
173+ }
174+
175+ disposable += execute ( state, input) . start { event in
176+ observer. action ( event. mapError ( ActionError . producerFailed) )
177+ action. eventsObserver. send ( value: event)
178+ }
179+
180+ disposable. add {
181+ actionState. modify { state in
182+ switch state. availability {
183+ case . disabledIdle, . enabledIdle:
184+ fatalError ( " The `Action` should not be idle at this point. " )
185+
186+ case . disabledExecuting:
187+ state. availability = . disabledIdle
188+
189+ case . enabledExecuting:
190+ state. availability = . enabledIdle
191+ isEnabled. value = true
192+ }
193+ isExecuting. value = false
194+ }
195+ }
196+ }
197+ }
116198 }
117199
118200 /// Initializes an `Action` that would be conditionally enabled.
@@ -122,11 +204,11 @@ public final class Action<Input, Output, Error: Swift.Error> {
122204 /// `execute` with the input value.
123205 ///
124206 /// - parameters:
125- /// - enabledIf : A property which determines the availability of the `Action`.
207+ /// - isEnabled : A property which determines the availability of the `Action`.
126208 /// - execute: A closure that produces a unit of work, as `SignalProducer`, to be
127209 /// executed by the `Action`.
128- public convenience init < P: PropertyProtocol > ( enabledIf property : P , execute: @escaping ( Input ) -> SignalProducer < Output , Error > ) where P. Value == Bool {
129- self . init ( state: property , enabledIf: { $0 } ) { _, input in
210+ public convenience init < P: PropertyProtocol > ( enabledIf isEnabled : P , execute: @escaping ( Input ) -> SignalProducer < Output , Error > ) where P. Value == Bool {
211+ self . init ( state: isEnabled , enabledIf: { $0 } ) { _, input in
130212 execute ( input)
131213 }
132214 }
@@ -162,62 +244,7 @@ public final class Action<Input, Output, Error: Swift.Error> {
162244 /// - returns: A producer that forwards events generated by its started unit of work,
163245 /// or emits `ActionError.disabled` if the execution attempt is failed.
164246 public func apply( _ input: Input ) -> SignalProducer < Output , ActionError < Error > > {
165- return SignalProducer { observer, disposable in
166- let startingState = self . state. modify { state -> Any ? in
167- if state. isEnabled {
168- state. isExecuting = true
169- return state. value
170- } else {
171- return nil
172- }
173- }
174-
175- guard let state = startingState else {
176- observer. send ( error: . disabled)
177- self . disabledErrorsObserver. send ( value: ( ) )
178- return
179- }
180-
181- self . executeClosure ( state, input) . startWithSignal { signal, signalDisposable in
182- disposable += signalDisposable
183-
184- signal. observe { event in
185- observer. action ( event. mapError ( ActionError . producerFailed) )
186- self . eventsObserver. send ( value: event)
187- }
188- }
189-
190- disposable += {
191- self . state. modify {
192- $0. isExecuting = false
193- }
194- }
195- }
196- }
197- }
198-
199- private struct ActionState {
200- var isExecuting : Bool = false
201-
202- var value : Any {
203- didSet {
204- userEnabled = userEnabledClosure ( value)
205- }
206- }
207-
208- private var userEnabled : Bool
209- private let userEnabledClosure : ( Any ) -> Bool
210-
211- init ( value: Any , isEnabled: @escaping ( Any ) -> Bool ) {
212- self . value = value
213- self . userEnabled = isEnabled ( value)
214- self . userEnabledClosure = isEnabled
215- }
216-
217- /// Whether the action should be enabled for the given combination of user
218- /// enabledness and executing status.
219- fileprivate var isEnabled : Bool {
220- return userEnabled && !isExecuting
247+ return execute ( self , input)
221248 }
222249}
223250
0 commit comments