Skip to content

Instantly share code, notes, and snippets.

@vic
Created August 12, 2022 10:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vic/a59a909f5415497cc4b71bab57cba770 to your computer and use it in GitHub Desktop.
Save vic/a59a909f5415497cc4b71bab57cba770 to your computer and use it in GitHub Desktop.
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