Skip to content

Commit

Permalink
Use local storage to save state (#1134)
Browse files Browse the repository at this point in the history
* Use local storage to save state

* Add .jvmopts

* try to get ci to not OOM

* fix ci.yaml
  • Loading branch information
johnynek authored Feb 15, 2024
1 parent b73c2b9 commit f48ff56
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 39 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ jobs:
with:
java-version: "${{matrix.java}}"
- name: "run coreJS tests"
run: "sbt \"++${{matrix.scala}} coreJS/test; jsapiJS/compile; jsuiJS/compile\""
run: |
sbt "++${{matrix.scala}} coreJS/test; jsapiJS/compile"
sbt "++${{matrix.scala}} jsuiJS/test"
strategy:
matrix:
java:
Expand Down Expand Up @@ -109,5 +111,4 @@ jobs:
name: ci
on:
pull_request: {}
push: {}

4 changes: 4 additions & 0 deletions .jvmopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-Xms512M
-Xmx4096M
-Xss2M
-XX:MaxMetaspaceSize=1024M
11 changes: 5 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -212,28 +212,27 @@ lazy val jsapi =
lazy val jsapiJS = jsapi.js

lazy val jsui =
(crossProject(JSPlatform).crossType(CrossType.Pure) in file("jsui"))
(crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("jsui"))
.settings(
commonSettings,
//commonJsSettings,
commonJsSettings,
name := "bosatsu-jsui",
assembly / test := {},
scalaJSUseMainModuleInitializer := true,
libraryDependencies ++=
Seq(
cats.value,
decline.value,
ff4s.value,
scalaCheck.value % Test,
scalaTest.value % Test,
scalaTestPlusScalacheck.value % Test
munit.value % Test,
munitScalaCheck.value % Test,
)
)
.enablePlugins(ScalaJSPlugin)
.enablePlugins(ScalaJSBundlerPlugin)
.dependsOn(base, core)

lazy val jsuiJS = jsui.js
lazy val jsuiJVM = jsui.jvm

lazy val bench = project
.dependsOn(core.jvm)
Expand Down
2 changes: 1 addition & 1 deletion jsui/.js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 87 additions & 5 deletions jsui/src/main/scala/org/bykn/bosatsu/jsui/State.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package org.bykn.bosatsu.jsui

import scala.concurrent.duration.Duration
import io.circe.{Json, Encoder, Decoder}
import io.circe.syntax._
import io.circe.parser.decode

import cats.syntax.all._

sealed trait State

Expand All @@ -10,16 +15,93 @@ object State {
}
case object Init extends State
case class WithText(
editorText: String
editorText: String
) extends HasText

case class Compiling(previousState: HasText) extends State

case class Compiled(
editorText: String,
output: String,
compilationTime: Duration
editorText: String,
output: String,
compilationTime: Duration
) extends HasText

def init: State = Init
}

// Custom encoder for Duration to handle it as milliseconds
implicit val encodeDuration: Encoder[Duration] =
Encoder.instance(duration => Json.fromLong(duration.toNanos))

// Custom decoder for Duration from milliseconds
implicit val decodeDuration: Decoder[Duration] =
Decoder.instance(cursor => cursor.as[Long].map(Duration.fromNanos(_)))
// Encoder for the State trait
implicit val encodeState: Encoder[State] = Encoder.instance {
case Init => Json.obj("type" -> Json.fromString("Init"))
case wt: WithText => wt.asJson(encodeWithText)
case compiling: Compiling => compiling.asJson(encodeCompiling)
case compiled: Compiled => compiled.asJson(encodeCompiled)
}

// Decoders for HasText and its subtypes
implicit val decodeHasText: Decoder[HasText] = {
val decodeWithText: Decoder[WithText] = Decoder.instance { cursor =>
cursor.downField("editorText").as[String].map(WithText(_))
}
val decodeCompiled: Decoder[Compiled] = Decoder.instance { cursor =>
(
cursor.downField("editorText").as[String],
cursor.downField("output").as[String],
cursor.downField("compilationTime").as[Duration]
).mapN(Compiled(_, _, _))
}
Decoder.instance { cursor =>
cursor.downField("type").as[String].flatMap {
case "WithText" => cursor.as(decodeWithText)
case "Compiled" => cursor.as(decodeCompiled)
}
}
}

// Decoders for the State trait and its implementations
implicit val decodeState: Decoder[State] = Decoder.instance { cursor =>
cursor.downField("type").as[String].flatMap {
case "Init" => Right(Init)
case "Compiling" =>
cursor.downField("previousState").as[HasText].map(Compiling(_))
case _ => decodeHasText(cursor)
}
}

// Manual encoders for distinguishing types
implicit val encodeWithText: Encoder[WithText] =
Encoder.forProduct2("type", "editorText") { wt =>
("WithText", wt.editorText)
}

implicit val encodeCompiled: Encoder[Compiled] =
Encoder.forProduct4("type", "editorText", "output", "compilationTime") {
compiled =>
(
"Compiled",
compiled.editorText,
compiled.output,
compiled.compilationTime
)
}

implicit val encodeCompiling: Encoder[Compiling] =
Encoder.forProduct2("type", "previousState")(compiling =>
(
"Compiling",
compiling.previousState match {
case comp @ Compiled(_, _, _) => encodeCompiled(comp)
case wt @ WithText(_) => encodeWithText(wt)
}
)
)

def stateToJsonString(s: State): String = s.asJson.spaces2SortKeys
def stringToState(str: String): Either[Throwable, State] =
decode[State](str)
}
21 changes: 19 additions & 2 deletions jsui/src/main/scala/org/bykn/bosatsu/jsui/Store.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.bykn.bosatsu.jsui
import cats.effect.{IO, Resource}
import org.bykn.bosatsu.{MemoryMain, rankn}
import org.typelevel.paiges.Doc
import org.scalajs.dom.window.localStorage

object Store {
val memoryMain = new MemoryMain[Either[Throwable, *], String](_.split("/", -1).toList)
Expand Down Expand Up @@ -33,8 +34,21 @@ object Store {
res
}

def stateSetter(st: State): IO[Unit] =
IO {
localStorage.setItem("state", State.stateToJsonString(st))
}

def initialState: IO[State] =
IO(localStorage.getItem("state")).flatMap { init =>
if (init == null) IO.pure(State.Init)
else IO.fromEither(State.stringToState(init))
}

val value: Resource[IO, ff4s.Store[IO, State, Action]] =
ff4s.Store[IO, State, Action](State.Init) { store =>
for {
init <- Resource.liftK(initialState)
store <- ff4s.Store[IO, State, Action](init) { store =>
{
case Action.CodeEntered(text) =>
{
Expand All @@ -50,6 +64,7 @@ object Store {
case ht: State.HasText =>
val action =
for {
_ <- stateSetter(ht)
start <- IO.monotonic
output <- runCompile(ht.editorText)
end <- IO.monotonic
Expand All @@ -61,12 +76,14 @@ object Store {
case Action.CompileCompleted(result, dur) =>
{
case State.Compiling(ht) =>
(State.Compiled(ht.editorText, result, dur), None)
val next = State.Compiled(ht.editorText, result, dur)
(next, Some(stateSetter(next)))
case unexpected =>
// TODO send some error message
println(s"unexpected Complete: $result => $unexpected")
(unexpected, None)
}
}
}
} yield store
}
48 changes: 27 additions & 21 deletions jsui/src/main/scala/org/bykn/bosatsu/jsui/View.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,43 @@ object View {
val aboveOut =
div(cls := "grid-item", "Output")

val codeBox =
div(cls := "grid-item",
button("evaluate",
onClick := (_ => Some(Action.RunCompile))
),
textArea(`type` := "text",
cls := "codein",
onInput := {
te => Some(Action.CodeEntered(
te.currentTarget.asInstanceOf[HTMLTextAreaElement].value))
}
),
val codeBox = dsl.useState { state =>
val text = state match {
case ht: State.HasText => ht.editorText
case _ => ""
}
div(
cls := "grid-item",
button("evaluate", onClick := (_ => Some(Action.RunCompile))),
textArea(
`type` := "text",
cls := "codein",
value := text,
onInput := { te =>
Some(
Action.CodeEntered(
te.currentTarget.asInstanceOf[HTMLTextAreaElement].value
)
)
}
)
)
}

val outBox = dsl.useState {
case Compiled(_, output, dur) =>
case Compiled(_, output, dur) =>
div(
cls := "grid-item",
literal(s"<pre>$output</pre>"),
br(),
"completed in ",
dur.toMillis.toString,
" ms")
" ms"
)
case _ =>
div(cls := "grid-item")
}

div(cls := "grid-container",
aboveCode,
aboveOut,
codeBox,
outBox)

div(cls := "grid-container", aboveCode, aboveOut, codeBox, outBox)
}
}
}
38 changes: 38 additions & 0 deletions jsui/src/test/scala/org/bykn/bosatsu/jsui/StateTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.bykn.bosatsu.jsui

import org.scalacheck.{Gen, Prop}
import scala.concurrent.duration.Duration

class StateTest extends munit.ScalaCheckSuite {

val genState: Gen[State] = {
val genWithText: Gen[State.HasText] =
Gen.oneOf(
Gen.asciiStr.map(State.WithText),
Gen
.zip(
Gen.asciiStr,
Gen.asciiStr,
Gen.choose(0L, 1L << 10).map(Duration(_, "millis"))
)
.map { case (a, b, c) => State.Compiled(a, b, c) }
)

Gen.oneOf(
Gen.const(State.Init),
genWithText,
genWithText.map(State.Compiling(_))
)
}

property("json encoding/decoding works") {
Prop.forAll(genState) { state =>
val str = State.stateToJsonString(state)
assertEquals(
State.stringToState(str),
Right(state),
s"encoded $state to $str"
)
}
}
}
4 changes: 2 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ object Dependencies {
lazy val jawnParser = Def.setting("org.typelevel" %%% "jawn-parser" % "1.5.1")
lazy val jawnAst = Def.setting("org.typelevel" %%% "jawn-ast" % "1.5.1")
lazy val jython = Def.setting("org.python" % "jython-standalone" % "2.7.3")
lazy val munit = Def.setting("org.scalameta" %% "munit" % "1.0.0-M10")
lazy val munitScalaCheck = Def.setting("org.scalameta" %% "munit-scalacheck" % "1.0.0-M10")
lazy val munit = Def.setting("org.scalameta" %%% "munit" % "1.0.0-M10")
lazy val munitScalaCheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % "1.0.0-M10")
lazy val paiges = Def.setting("org.typelevel" %%% "paiges-core" % "0.4.3")
lazy val scalaCheck =
Def.setting("org.scalacheck" %%% "scalacheck" % "1.17.0")
Expand Down

0 comments on commit f48ff56

Please sign in to comment.