Skip to content

Commit

Permalink
Breaking up the runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Mar 25, 2024
1 parent 605495b commit 78e83ed
Showing 1 changed file with 96 additions and 61 deletions.
157 changes: 96 additions & 61 deletions tyrian/js/src/main/scala/tyrian/runtime/TyrianRuntime.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tyrian.runtime

import cats.effect.kernel.Async
import cats.effect.kernel.Ref
import cats.effect.std.AtomicCell
import cats.effect.std.Dispatcher
import cats.effect.std.Queue
Expand All @@ -27,72 +28,106 @@ object TyrianRuntime:
update: Model => Msg => (Model, Cmd[F, Msg]),
view: Model => Html[Msg],
subscriptions: Model => Sub[F, Msg]
)(using F: Async[F]): F[Nothing] = Dispatcher.sequential[F].use { dispatcher =>
(F.ref(ModelHolder(initModel, true)), AtomicCell[F].of(List.empty[(String, F[Unit])]), Queue.unbounded[F, Msg])
.flatMapN { (model, currentSubs, msgQueue) =>

def runCmd(cmd: Cmd[F, Msg]): F[Unit] =
CmdHelper.cmdToTaskList(cmd).foldMapM { task =>
task.handleError(_ => None).flatMap(_.traverse_(msgQueue.offer(_))).start.void
)(using F: Async[F]): F[Nothing] =
Dispatcher.sequential[F].use { dispatcher =>
val loop = mainLoop(dispatcher, router, node, initModel, initCmd, update, view, subscriptions)
val model = F.ref(ModelHolder(initModel, true))
val currentSubs = AtomicCell[F].of(List.empty[(String, F[Unit])])
val msgQueue = Queue.unbounded[F, Msg]

(model, currentSubs, msgQueue).flatMapN(loop)
}

def mainLoop[F[_], Model, Msg](
dispatcher: Dispatcher[F],
router: Location => Msg,
node: Element,
initModel: Model,
initCmd: Cmd[F, Msg],
update: Model => Msg => (Model, Cmd[F, Msg]),
view: Model => Html[Msg],
subscriptions: Model => Sub[F, Msg]
)(
model: Ref[F, ModelHolder[Model]],
currentSubs: AtomicCell[F, List[(String, F[Unit])]],
msgQueue: Queue[F, Msg]
)(using F: Async[F]): F[Nothing] =
val runCmd: Cmd[F, Msg] => F[Unit] = runCommands(msgQueue)
val runSub: Sub[F, Msg] => F[Unit] = runSubscriptions(currentSubs, msgQueue, dispatcher)
val onMsg: Msg => Unit = postMsg(dispatcher, msgQueue)

val renderLoop: F[Nothing] =
def redraw(vnode: VNode): F[VNode] =
model
.getAndUpdate(m => ModelHolder(m.model, false))
.flatMap { m =>
F.delay(Rendering.render(vnode, m.model, view, onMsg, router))
}

def runSub(sub: Sub[F, Msg]): F[Unit] =
currentSubs.evalUpdate { oldSubs =>
val allSubs = SubHelper.flatten(sub)
val (stillAlive, discarded) = SubHelper.aliveAndDead(allSubs, oldSubs)
def loop(vnode: VNode): F[Nothing] =
model.get.flatMap { m =>
if m.updated then requestAnimationFrame *> redraw(vnode).flatMap(loop)
else requestAnimationFrame *> loop(vnode)
}

val newSubs = SubHelper
.findNewSubs(allSubs, stillAlive.map(_._1), Nil)
.traverse(SubHelper.runObserve(_) { result =>
dispatcher.unsafeRunAndForget(
result.toOption.flatten.foldMapM(msgQueue.offer(_).void)
)
})
F.delay(toVNode(node)).flatMap(loop)

discarded.foldMapM(_.start.void) *> newSubs.map(_ ++ stillAlive)
val msgLoop: F[Nothing] =
msgQueue.take.flatMap { msg =>
model
.modify { case ModelHolder(oldModel, _) =>
val (newModel, cmd) = update(oldModel)(msg)
val sub = subscriptions(newModel)

(ModelHolder(newModel, true), (cmd, sub))
}
// end runSub

val msgLoop = msgQueue.take.flatMap { msg =>
model
.modify { case ModelHolder(oldModel, _) =>
val (newModel, cmd) = update(oldModel)(msg)
val sub = subscriptions(newModel)
(ModelHolder(newModel, true), (cmd, sub))
}
.flatMap { (cmd, sub) =>
runCmd(cmd) *> runSub(sub)
}
.void
}.foreverM
// end msgLoop

val renderLoop =
val onMsg = (msg: Msg) => dispatcher.unsafeRunAndForget(msgQueue.offer(msg))

@nowarn("msg=discarded")
val requestAnimationFrame = F.async_ { cb =>
dom.window.requestAnimationFrame(_ => cb(Either.unit))
()
.flatMap { (cmd, sub) =>
runCmd(cmd) *> runSub(sub)
}
.void
}.foreverM

def redraw(vnode: VNode) =
model.getAndUpdate(m => ModelHolder(m.model, false)).flatMap { m =>
if m.updated then F.delay(Rendering.render(vnode, m.model, view, onMsg, router))
else F.pure(vnode)
}

def loop(vnode: VNode): F[Nothing] =
requestAnimationFrame *> redraw(vnode).flatMap(loop(_))

F.delay(toVNode(node)).flatMap(loop)
// end renderLoop

renderLoop.background.surround {
msgLoop.background.surround {
runCmd(initCmd) *> F.never
}
}
renderLoop.background.surround {
msgLoop.background.surround {
runCmd(initCmd) *> F.never
}

}
}

def runCommands[F[_], Msg](msgQueue: Queue[F, Msg])(cmd: Cmd[F, Msg])(using F: Async[F]): F[Unit] =
CmdHelper.cmdToTaskList(cmd).foldMapM { task =>
task.handleError(_ => None).flatMap(_.traverse_(msgQueue.offer(_))).start.void
}

def runSubscriptions[F[_], Msg](
currentSubs: AtomicCell[F, List[(String, F[Unit])]],
msgQueue: Queue[F, Msg],
dispatcher: Dispatcher[F]
)(sub: Sub[F, Msg])(using F: Async[F]): F[Unit] =
currentSubs.evalUpdate { oldSubs =>
val allSubs = SubHelper.flatten(sub)
val (stillAlive, discarded) = SubHelper.aliveAndDead(allSubs, oldSubs)

val newSubs = SubHelper
.findNewSubs(allSubs, stillAlive.map(_._1), Nil)
.traverse(
SubHelper.runObserve(_) { result =>
dispatcher.unsafeRunAndForget(
result.toOption.flatten.foldMapM(msgQueue.offer(_).void)
)
}
)

discarded.foldMapM(_.start.void) *> newSubs.map(_ ++ stillAlive)
}

// Triggers another render tick, but otherwise does nothing.
@nowarn("msg=discarded")
def requestAnimationFrame[F[_]](using F: Async[F]): F[Unit] =
F.async_ { cb =>
dom.window.requestAnimationFrame(_ => cb(Either.unit))
()
}

// Send messages to the queue.
def postMsg[F[_], Msg](dispatcher: Dispatcher[F], msgQueue: Queue[F, Msg]): Msg => Unit =
msg => dispatcher.unsafeRunAndForget(msgQueue.offer(msg))

0 comments on commit 78e83ed

Please sign in to comment.