Skip to content

Commit

Permalink
Propagate even with state for feedbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
sergdort committed Jun 2, 2020
1 parent 49404fc commit 22574a4
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 59 deletions.
16 changes: 11 additions & 5 deletions Example/UnifiedStoreUIKitExample/UnifiedStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ enum UnifiedStore {
)
)

private static let feedbacks: Loop<State, Event>.Feedback = Loop<State, Event>.Feedback.combine(
Loop<State, Event>.Feedback.pullback(
feedback: Movies.feedback,
private static let feedbacks: Loop<State, Event>.Feedback = Movies.feedback
.pullback(
value: \.movies,
event: Event.movies
embedEvent: Event.movies,
extractEvent: { (event) -> Movies.Event? in
switch event {
case let .movies(moviesEvent):
return moviesEvent
default:
return nil
}
}
)
)
}

extension UnifiedStore {
Expand Down
17 changes: 12 additions & 5 deletions Loop/Floodgate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ final class Floodgate<State, Event>: FeedbackEventConsumer<Event> {
}
}

let (stateDidChange, changeObserver) = Signal<State, Never>.pipe()
let (stateDidChange, changeObserver) = Signal<(State, Event?), Never>.pipe()

/// Replay the current value, and then publish the subsequent changes.
var producer: SignalProducer<State, Never> {
return feedbackProducer.map(\.0)
}

private var feedbackProducer: SignalProducer<(State, Event?), Never> {
SignalProducer { observer, lifetime in
self.withValue { initial, hasStarted -> Void in
observer.send(value: initial)
observer.send(value: (initial, nil))
lifetime += self.stateDidChange.observe(observer)
}
}
Expand All @@ -40,10 +44,13 @@ final class Floodgate<State, Event>: FeedbackEventConsumer<Event> {
dispose()
}

func bootstrap(with feedbacks: [FeedbackLoop<State, Event>.Feedback]) {
func bootstrap(with feedbacks: [Loop<State, Event>.Feedback]) {
for feedback in feedbacks {
// Pass `producer` which has replay-1 semantic.
feedbackDisposables += feedback.events(producer, self)
feedbackDisposables += feedback.events(
feedbackProducer,
self
)
}

reducerLock.perform {
Expand Down Expand Up @@ -122,6 +129,6 @@ final class Floodgate<State, Event>: FeedbackEventConsumer<Event> {

private func consume(_ event: Event) {
reducer(&state, event)
changeObserver.send(value: state)
changeObserver.send(value: (state, event))
}
}
233 changes: 184 additions & 49 deletions Loop/Public/FeedbackLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import ReactiveSwift

extension Loop {
public struct Feedback {
let events: (_ state: SignalProducer<State, Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable
let events: (_ state: SignalProducer<(State, Event?), Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable

/// Private designated initializer. See the public designated initializer below.
fileprivate init(
startWith events: @escaping (_ state: SignalProducer<State, Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable
startWith events: @escaping (_ state: SignalProducer<(State, Event?), Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable
) {
self.events = events
}
Expand Down Expand Up @@ -81,7 +81,7 @@ extension Loop {
/// and having them consumed by `output` using the `SignalProducer.enqueue(to:)` operator.
public init(
events: @escaping (
_ state: SignalProducer<State, Never>,
_ state: SignalProducer<(State, Event?), Never>,
_ output: FeedbackEventConsumer<Event>
) -> SignalProducer<Never, Never>
) {
Expand Down Expand Up @@ -118,11 +118,51 @@ extension Loop {
compacting transform: @escaping (SignalProducer<State, Never>) -> SignalProducer<U, Never>,
effects: @escaping (U) -> Effect
) where Effect.Value == Event, Effect.Error == Never {
self.events = { state, output in
events = { state, output in
// NOTE: `observe(on:)` should be applied on the inner producers, so
// that cancellation due to state changes would be able to
// cancel outstanding events that have already been scheduled.
transform(state)
transform(state.map(\.0))
.flatMap(.latest) { effects($0).producer.enqueue(to: output) }
.start()
}
}

/// Creates a Feedback which re-evaluates the given effect every time the
/// `Signal` derived from the latest state yields a new value.
///
/// If the previous effect is still alive when a new one is about to start,
/// the previous one would automatically be cancelled.
///
/// - parameters:
/// - transform: The transform which derives a `Signal` of values from the
/// latest state.
/// - effects: The side effect accepting transformed values produced by
/// `transform` and yielding events that eventually affect
/// the state.
public static func compacting<U, Effect: SignalProducerConvertible>(
state transform: @escaping (SignalProducer<State, Never>) -> SignalProducer<U, Never>,
effects: @escaping (U) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
return Feedback { state, output in
// NOTE: `enqueue(to:)` should be applied on the inner producers, so
// that cancellation due to state changes would be able to
// cancel outstanding events that have already been scheduled.
transform(state.map(\.0))
.flatMap(.latest) { effects($0).producer.enqueue(to: output) }
.start()
}
}

public static func compacting<U, Effect: SignalProducerConvertible>(
events transform: @escaping (SignalProducer<Event, Never>) -> SignalProducer<U, Never>,
effects: @escaping (U) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
return Feedback { state, output in
// NOTE: `enqueue(to:)` should be applied on the inner producers, so
// that cancellation due to state changes would be able to
// cancel outstanding events that have already been scheduled.
transform(state.compactMap(\.1))
.flatMap(.latest) { effects($0).producer.enqueue(to: output) }
.start()
}
Expand All @@ -144,8 +184,30 @@ extension Loop {
skippingRepeated transform: @escaping (State) -> Control?,
effects: @escaping (Control) -> Effect
) where Effect.Value == Event, Effect.Error == Never {
self.init(compacting: { $0.map(transform).skipRepeats() },
effects: { $0.map(effects)?.producer ?? .empty })
self.init(
compacting: { $0.map(transform).skipRepeats() },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

public static func skippingRepeated<Control: Equatable, Effect: SignalProducerConvertible>(
state transform: @escaping (State) -> Control?,
effects: @escaping (Control) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
compacting(
state: { $0.map(transform).skipRepeats() },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

public static func skippingRepeated<Payload: Equatable, Effect: SignalProducerConvertible>(
events transform: @escaping (Event) -> Payload?,
effects: @escaping (Payload) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
compacting(
events: { $0.map(transform).skipRepeats() },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

/// Creates a Feedback which re-evaluates the given effect every time the
Expand All @@ -163,10 +225,44 @@ extension Loop {
lensing transform: @escaping (State) -> Control?,
effects: @escaping (Control) -> Effect
) where Effect.Value == Event, Effect.Error == Never {
self.init(compacting: { $0.map(transform) },
effects: { $0.map(effects)?.producer ?? .empty })
self.init(
compacting: { $0.map(transform) },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

/// Creates a Feedback which re-evaluates the given effect every time the
/// state changes.
///
/// If the previous effect is still alive when a new one is about to start,
/// the previous one would automatically be cancelled.
///
/// - parameters:
/// - transform: The transform to apply on the state.
/// - effects: The side effect accepting transformed values produced by
/// `transform` and yielding events that eventually affect
/// the state.

public static func lensing<Control, Effect: SignalProducerConvertible>(
state transform: @escaping (State) -> Control?,
effects: @escaping (Control) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
compacting(
state: { $0.map(transform) },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

public static func extracting<Payload, Effect: SignalProducerConvertible>(
payload transform: @escaping (Event) -> Payload?,
effects: @escaping (Payload) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
compacting(
events: { $0.map(transform) },
effects: { $0.map(effects)?.producer ?? .empty }
)
}

/// Creates a Feedback which re-evaluates the given effect every time the
/// given predicate passes.
///
Expand All @@ -181,12 +277,36 @@ extension Loop {
predicate: @escaping (State) -> Bool,
effects: @escaping (State) -> Effect
) where Effect.Value == Event, Effect.Error == Never {
self.init(compacting: { $0 },
effects: { state -> SignalProducer<Event, Never> in
predicate(state) ? effects(state).producer : .empty
})
self.init(
compacting: { $0 },
effects: { state -> SignalProducer<Event, Never> in
predicate(state) ? effects(state).producer : .empty
}
)
}


/// Creates a Feedback which re-evaluates the given effect every time the
/// given predicate passes.
///
/// If the previous effect is still alive when a new one is about to start,
/// the previous one would automatically be cancelled.
///
/// - parameters:
/// - predicate: The predicate to apply on the state.
/// - effects: The side effect accepting the state and yielding events
/// that eventually affect the state.
public static func predicate<Effect: SignalProducerConvertible>(
state predicate: @escaping (State) -> Bool,
effects: @escaping (State) -> Effect
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
compacting(
state: { $0 },
effects: { state -> SignalProducer<Event, Never> in
predicate(state) ? effects(state).producer : .empty
}
)
}

/// Creates a Feedback which re-evaluates the given effect every time the
/// state changes.
///
Expand All @@ -202,56 +322,71 @@ extension Loop {
self.init(compacting: { $0 }, effects: effects)
}

/// Creates a Feedback which re-evaluates the given effect every time the
/// state changes with the Event that caused the change.
///
/// If the previous effect is still alive when a new one is about to start,
/// the previous one would automatically be cancelled.
///
/// - parameters:
/// - effects: The side effect accepting the state and yielding events
/// that eventually affect the state.
public static func middleware<Effect: SignalProducerConvertible>(
effect: @escaping (State, Event) -> Effect
) -> Self where Effect.Value == Event, Effect.Error == Never {
Feedback { (state, output) -> Disposable in
state.compactMap { s, e -> (State, Event)? in
guard let e = e else {
return nil
}
return (s, e)
}
.flatMap(.latest) {
effect($0, $1).producer.enqueue(to: output)
}
.start()
}
}

public static var input: (feedback: Feedback, observer: (Event) -> Void) {
let pipe = Signal<Event, Never>.pipe()
let feedback = Feedback(source: pipe.output, as: { $0 })
let feedback = Feedback(source: pipe.output) { $0 }
return (feedback, pipe.input.send)
}

public static func pullback<LocalState, LocalEvent>(
feedback: Loop<LocalState, LocalEvent>.Feedback,
value: KeyPath<State, LocalState>,
event: @escaping (LocalEvent) -> Event
embedEvent: @escaping (LocalEvent) -> Event,
extractEvent: @escaping (Event) -> LocalEvent?
) -> Feedback {
return Feedback(startWith: { (state, consumer) in
return feedback.events(
state.map(value),
consumer.pullback(event)
)
})
return feedback.pullback(value: value, embedEvent: embedEvent, extractEvent: extractEvent)
}

public func pullback<GlobalState, GlobalEvent>(
value: KeyPath<GlobalState, State>,
embedEvent: @escaping (Event) -> GlobalEvent,
extractEvent: @escaping (GlobalEvent) -> Event?
) -> Loop<GlobalState, GlobalEvent>.Feedback {
return Loop<GlobalState, GlobalEvent>.Feedback { (state, consumer) in
self.events(
state.map {
($0.0[keyPath: value], $0.1.flatMap(extractEvent))
},
consumer.pullback(embedEvent)
)
}
}

public static func combine(_ feedbacks: Loop<State, Event>.Feedback...) -> Feedback {
return Feedback(startWith: { (state, consumer) in
return feedbacks.map { (feedback) in
return Feedback { state, consumer in
feedbacks.map { feedback in
feedback.events(state, consumer)
}
.reduce(into: CompositeDisposable()) { (composite, disposable) in
.reduce(into: CompositeDisposable()) { composite, disposable in
composite += disposable
}
})
}
}
}
}

extension Loop.Feedback {
@available(*, deprecated, renamed:"init(_:)")
public static func custom(
_ setup: @escaping (
_ state: SignalProducer<State, Never>,
_ output: FeedbackEventConsumer<Event>
) -> Disposable
) -> Loop.Feedback {
return FeedbackLoop.Feedback(events: setup)
}

@available(*, deprecated, renamed:"init(_:)")
public init(
events: @escaping (
_ state: SignalProducer<State, Never>,
_ output: FeedbackEventConsumer<Event>
) -> Disposable
) {
self.events = { events($0.producer, $1) }
}
}
1 change: 1 addition & 0 deletions LoopTests/FeedbackLoopSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class FeedbackLoopSystemTests: XCTestCase {
// `state` is NOT GUARANTEED to reflect events emitted earlier in the producer chain.
state
.take(first: 3)
.map(\.0)
.map { $0 + 1000 }
)
.enqueue(to: output)
Expand Down

0 comments on commit 22574a4

Please sign in to comment.