Created
August 12, 2022 10:13
-
-
Save vic/a59a909f5415497cc4b71bab57cba770 to your computer and use it in GitHub Desktop.
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
import com.raquo.airstream.flatten.FlattenStrategy | |
import com.raquo.laminar.api.L._ | |
import com.raquo.laminar.nodes.ReactiveElement | |
import com.raquo.laminar.nodes.ReactiveHtmlElement | |
import scala.util.Failure | |
import scala.util.Success | |
import scala.util.Try | |
import org.scalajs.dom | |
trait Reducer[-In, State, +Out] extends Sink[In] with EventSource[Out] { | |
protected[Reducer] def initialState: State | |
protected[Reducer] def initialHandler: Flow.Handler | |
def stateSignal: Signal[State] = Flow.stateSignal | |
override def toObserver: Observer[In] = Flow.eventBus.writer.contramap(new Reducer.Event.In(_)) | |
override def toObservable: EventStream[Out] = Flow.outStream | |
def bind[E <: ReactiveElement.Base] = Flow.bind | |
private[this] object Flow { | |
type Handler = Reducer.Handler[_ >: In, State, _ <: Out] | |
type Event = Reducer.Event[_ >: In, State, _ <: Out] | |
type InEvent = Reducer.Event.In[In, State, Out] | |
type OutEvent = Reducer.Event.Out[In, State, Out] | |
type UpdateStateEvent = Reducer.Event.UpdateState[In, State, Out] | |
type ReplaceHandlerEvent = Reducer.Event.ReplaceHandler[In, State, Out] | |
val eventBus: EventBus[Event] = new EventBus() | |
val outStream: EventStream[Out] = | |
eventBus.events.collect { case e: OutEvent => e.out } | |
val stateSignal: Signal[State] = | |
eventBus.events | |
.collect { case e: UpdateStateEvent => e.f } | |
.foldLeftRecover(Try(initialState)) { | |
case (Success(currentState), Success(stateTransformer)) => Try(stateTransformer(currentState)) | |
case (_, Failure(exception)) => Failure(exception) | |
case (Failure(exception), _) => Failure(exception) | |
} | |
val handlerSignal: Signal[Handler] = | |
eventBus.events | |
.collect { case e: ReplaceHandlerEvent => e.h } | |
.foldLeftRecover(Try(initialHandler)) { | |
case (_, Success(replacement)) => Success(replacement) | |
case (_, Failure(exception)) => Failure(exception) | |
case (current, _) => current | |
} | |
val loopBackEvents: EventStream[Event] = | |
eventBus.events | |
.collect { case e: InEvent => e.in } | |
.compose { ins => | |
val firstHandler = EventStream.fromValue((), emitOnce = true).sample(handlerSignal) | |
val handlerStream = EventStream.merge(firstHandler, handlerSignal.changes) | |
handlerStream.flatMap(handler => handler(ins))(FlattenStrategy.SwitchStreamStrategy) | |
} | |
val noopEvents: EventStream[Unit] = | |
eventBus.events | |
.collect { case e: Reducer.Event.Noop[_, _, _] => () } | |
def bind[E <: ReactiveElement.Base]: Mod[E] = Seq( | |
loopBackEvents --> eventBus.writer, | |
// we just want to make sure all sources are connected even if user does not consumes ie, outStream. | |
noopEvents --> Observer.empty, | |
outStream --> Observer.empty, | |
stateSignal --> Observer.empty | |
) | |
} | |
} | |
object Reducer { | |
type Handler[-I, S, +O] = EventStream[I] => EventStream[Event[I, S, O]] | |
object Handler {} | |
sealed trait Event[-I, S, +O] | |
object Event { | |
final class Noop[I, S, O] extends Event[I, S, O] | |
final class In[I, S, O](val in: I) extends Event[I, S, O] | |
final class Out[I, S, O](val out: O) extends Event[I, S, O] | |
final class UpdateState[I, S, O](val f: S => S) extends Event[I, S, O] | |
final class CurrentState[I, S, O] extends Event[I, S, O] | |
final class ReplaceHandler[I, S, O](val h: Handler[I, S, O]) extends Event[I, S, O] | |
def StreamEmitter[I, S, O]: StreamEmitter[I, S, O] = new StreamEmitter | |
final class StreamEmitter[-I, S, +O] {} | |
def Emitter[I, S, O]: Emitter[I, S, O] = new Emitter | |
final class Emitter[I, S, O] { | |
def noop: Noop[I, S, O] = new Noop | |
def updateState(f: S => S): UpdateState[I, S, O] = new UpdateState(f) | |
def loopBack(i: I): In[I, S, O] = new In(i) | |
def output(o: O): Out[I, S, O] = new Out(o) | |
} | |
} | |
def fromStateFunction[I, S](updateState: I => S => S)(initialState: => S): Reducer[I, S, Nothing] = | |
composeStream[I, S, Nothing] { emitter => ins => | |
ins.map { in => emitter.updateState(updateState(in)) } | |
}(initialState) | |
def composeStream[I, S, O](handler: Event.Emitter[I, S, O] => Handler[I, S, O])(initialState: => S): Reducer[I, S, O] = { | |
lazy val initial = initialState | |
lazy val emitter = new Event.Emitter[I, S, O] | |
new Reducer[I, S, O] { | |
override protected def initialState: S = initial | |
override protected def initialHandler: EventStream[I] => EventStream[Event[I, S, O]] = handler(emitter) | |
} | |
} | |
trait Component[+R <: dom.html.Element, -I, S, +O] extends io.laminext.base.ComponentBase[R] | |
object Component { | |
def apply[I, S, O, R <: dom.html.Element](view: Reducer[I, S, O] => ReactiveHtmlElement[R])(reducer: Reducer[I, S, O]): Component[R, I, S, O] = { | |
new Component[R, I, S, O] { | |
override val el: ReactiveHtmlElement[R] = view(reducer).amend(reducer.bind) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment