Skip to content

Commit f48ff56

Browse files
authored
Use local storage to save state (#1134)
* Use local storage to save state * Add .jvmopts * try to get ci to not OOM * fix ci.yaml
1 parent b73c2b9 commit f48ff56

File tree

9 files changed

+186
-39
lines changed

9 files changed

+186
-39
lines changed

.github/workflows/ci.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ jobs:
5858
with:
5959
java-version: "${{matrix.java}}"
6060
- name: "run coreJS tests"
61-
run: "sbt \"++${{matrix.scala}} coreJS/test; jsapiJS/compile; jsuiJS/compile\""
61+
run: |
62+
sbt "++${{matrix.scala}} coreJS/test; jsapiJS/compile"
63+
sbt "++${{matrix.scala}} jsuiJS/test"
6264
strategy:
6365
matrix:
6466
java:
@@ -109,5 +111,4 @@ jobs:
109111
name: ci
110112
on:
111113
pull_request: {}
112-
push: {}
113114

.jvmopts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-Xms512M
2+
-Xmx4096M
3+
-Xss2M
4+
-XX:MaxMetaspaceSize=1024M

build.sbt

+5-6
Original file line numberDiff line numberDiff line change
@@ -212,28 +212,27 @@ lazy val jsapi =
212212
lazy val jsapiJS = jsapi.js
213213

214214
lazy val jsui =
215-
(crossProject(JSPlatform).crossType(CrossType.Pure) in file("jsui"))
215+
(crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("jsui"))
216216
.settings(
217217
commonSettings,
218-
//commonJsSettings,
218+
commonJsSettings,
219219
name := "bosatsu-jsui",
220-
assembly / test := {},
221220
scalaJSUseMainModuleInitializer := true,
222221
libraryDependencies ++=
223222
Seq(
224223
cats.value,
225-
decline.value,
226224
ff4s.value,
227225
scalaCheck.value % Test,
228-
scalaTest.value % Test,
229-
scalaTestPlusScalacheck.value % Test
226+
munit.value % Test,
227+
munitScalaCheck.value % Test,
230228
)
231229
)
232230
.enablePlugins(ScalaJSPlugin)
233231
.enablePlugins(ScalaJSBundlerPlugin)
234232
.dependsOn(base, core)
235233

236234
lazy val jsuiJS = jsui.js
235+
lazy val jsuiJVM = jsui.jvm
237236

238237
lazy val bench = project
239238
.dependsOn(core.jvm)

jsui/.js/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package org.bykn.bosatsu.jsui
22

33
import scala.concurrent.duration.Duration
4+
import io.circe.{Json, Encoder, Decoder}
5+
import io.circe.syntax._
6+
import io.circe.parser.decode
7+
8+
import cats.syntax.all._
49

510
sealed trait State
611

@@ -10,16 +15,93 @@ object State {
1015
}
1116
case object Init extends State
1217
case class WithText(
13-
editorText: String
18+
editorText: String
1419
) extends HasText
1520

1621
case class Compiling(previousState: HasText) extends State
1722

1823
case class Compiled(
19-
editorText: String,
20-
output: String,
21-
compilationTime: Duration
24+
editorText: String,
25+
output: String,
26+
compilationTime: Duration
2227
) extends HasText
2328

2429
def init: State = Init
25-
}
30+
31+
// Custom encoder for Duration to handle it as milliseconds
32+
implicit val encodeDuration: Encoder[Duration] =
33+
Encoder.instance(duration => Json.fromLong(duration.toNanos))
34+
35+
// Custom decoder for Duration from milliseconds
36+
implicit val decodeDuration: Decoder[Duration] =
37+
Decoder.instance(cursor => cursor.as[Long].map(Duration.fromNanos(_)))
38+
// Encoder for the State trait
39+
implicit val encodeState: Encoder[State] = Encoder.instance {
40+
case Init => Json.obj("type" -> Json.fromString("Init"))
41+
case wt: WithText => wt.asJson(encodeWithText)
42+
case compiling: Compiling => compiling.asJson(encodeCompiling)
43+
case compiled: Compiled => compiled.asJson(encodeCompiled)
44+
}
45+
46+
// Decoders for HasText and its subtypes
47+
implicit val decodeHasText: Decoder[HasText] = {
48+
val decodeWithText: Decoder[WithText] = Decoder.instance { cursor =>
49+
cursor.downField("editorText").as[String].map(WithText(_))
50+
}
51+
val decodeCompiled: Decoder[Compiled] = Decoder.instance { cursor =>
52+
(
53+
cursor.downField("editorText").as[String],
54+
cursor.downField("output").as[String],
55+
cursor.downField("compilationTime").as[Duration]
56+
).mapN(Compiled(_, _, _))
57+
}
58+
Decoder.instance { cursor =>
59+
cursor.downField("type").as[String].flatMap {
60+
case "WithText" => cursor.as(decodeWithText)
61+
case "Compiled" => cursor.as(decodeCompiled)
62+
}
63+
}
64+
}
65+
66+
// Decoders for the State trait and its implementations
67+
implicit val decodeState: Decoder[State] = Decoder.instance { cursor =>
68+
cursor.downField("type").as[String].flatMap {
69+
case "Init" => Right(Init)
70+
case "Compiling" =>
71+
cursor.downField("previousState").as[HasText].map(Compiling(_))
72+
case _ => decodeHasText(cursor)
73+
}
74+
}
75+
76+
// Manual encoders for distinguishing types
77+
implicit val encodeWithText: Encoder[WithText] =
78+
Encoder.forProduct2("type", "editorText") { wt =>
79+
("WithText", wt.editorText)
80+
}
81+
82+
implicit val encodeCompiled: Encoder[Compiled] =
83+
Encoder.forProduct4("type", "editorText", "output", "compilationTime") {
84+
compiled =>
85+
(
86+
"Compiled",
87+
compiled.editorText,
88+
compiled.output,
89+
compiled.compilationTime
90+
)
91+
}
92+
93+
implicit val encodeCompiling: Encoder[Compiling] =
94+
Encoder.forProduct2("type", "previousState")(compiling =>
95+
(
96+
"Compiling",
97+
compiling.previousState match {
98+
case comp @ Compiled(_, _, _) => encodeCompiled(comp)
99+
case wt @ WithText(_) => encodeWithText(wt)
100+
}
101+
)
102+
)
103+
104+
def stateToJsonString(s: State): String = s.asJson.spaces2SortKeys
105+
def stringToState(str: String): Either[Throwable, State] =
106+
decode[State](str)
107+
}

jsui/src/main/scala/org/bykn/bosatsu/jsui/Store.scala

+19-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.bykn.bosatsu.jsui
33
import cats.effect.{IO, Resource}
44
import org.bykn.bosatsu.{MemoryMain, rankn}
55
import org.typelevel.paiges.Doc
6+
import org.scalajs.dom.window.localStorage
67

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

37+
def stateSetter(st: State): IO[Unit] =
38+
IO {
39+
localStorage.setItem("state", State.stateToJsonString(st))
40+
}
41+
42+
def initialState: IO[State] =
43+
IO(localStorage.getItem("state")).flatMap { init =>
44+
if (init == null) IO.pure(State.Init)
45+
else IO.fromEither(State.stringToState(init))
46+
}
47+
3648
val value: Resource[IO, ff4s.Store[IO, State, Action]] =
37-
ff4s.Store[IO, State, Action](State.Init) { store =>
49+
for {
50+
init <- Resource.liftK(initialState)
51+
store <- ff4s.Store[IO, State, Action](init) { store =>
3852
{
3953
case Action.CodeEntered(text) =>
4054
{
@@ -50,6 +64,7 @@ object Store {
5064
case ht: State.HasText =>
5165
val action =
5266
for {
67+
_ <- stateSetter(ht)
5368
start <- IO.monotonic
5469
output <- runCompile(ht.editorText)
5570
end <- IO.monotonic
@@ -61,12 +76,14 @@ object Store {
6176
case Action.CompileCompleted(result, dur) =>
6277
{
6378
case State.Compiling(ht) =>
64-
(State.Compiled(ht.editorText, result, dur), None)
79+
val next = State.Compiled(ht.editorText, result, dur)
80+
(next, Some(stateSetter(next)))
6581
case unexpected =>
6682
// TODO send some error message
6783
println(s"unexpected Complete: $result => $unexpected")
6884
(unexpected, None)
6985
}
7086
}
7187
}
88+
} yield store
7289
}

jsui/src/main/scala/org/bykn/bosatsu/jsui/View.scala

+27-21
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,43 @@ object View {
1616
val aboveOut =
1717
div(cls := "grid-item", "Output")
1818

19-
val codeBox =
20-
div(cls := "grid-item",
21-
button("evaluate",
22-
onClick := (_ => Some(Action.RunCompile))
23-
),
24-
textArea(`type` := "text",
25-
cls := "codein",
26-
onInput := {
27-
te => Some(Action.CodeEntered(
28-
te.currentTarget.asInstanceOf[HTMLTextAreaElement].value))
29-
}
30-
),
19+
val codeBox = dsl.useState { state =>
20+
val text = state match {
21+
case ht: State.HasText => ht.editorText
22+
case _ => ""
23+
}
24+
div(
25+
cls := "grid-item",
26+
button("evaluate", onClick := (_ => Some(Action.RunCompile))),
27+
textArea(
28+
`type` := "text",
29+
cls := "codein",
30+
value := text,
31+
onInput := { te =>
32+
Some(
33+
Action.CodeEntered(
34+
te.currentTarget.asInstanceOf[HTMLTextAreaElement].value
35+
)
36+
)
37+
}
38+
)
3139
)
40+
}
3241

3342
val outBox = dsl.useState {
34-
case Compiled(_, output, dur) =>
43+
case Compiled(_, output, dur) =>
3544
div(
3645
cls := "grid-item",
3746
literal(s"<pre>$output</pre>"),
3847
br(),
3948
"completed in ",
4049
dur.toMillis.toString,
41-
" ms")
50+
" ms"
51+
)
4252
case _ =>
4353
div(cls := "grid-item")
4454
}
45-
46-
div(cls := "grid-container",
47-
aboveCode,
48-
aboveOut,
49-
codeBox,
50-
outBox)
55+
56+
div(cls := "grid-container", aboveCode, aboveOut, codeBox, outBox)
5157
}
52-
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.bykn.bosatsu.jsui
2+
3+
import org.scalacheck.{Gen, Prop}
4+
import scala.concurrent.duration.Duration
5+
6+
class StateTest extends munit.ScalaCheckSuite {
7+
8+
val genState: Gen[State] = {
9+
val genWithText: Gen[State.HasText] =
10+
Gen.oneOf(
11+
Gen.asciiStr.map(State.WithText),
12+
Gen
13+
.zip(
14+
Gen.asciiStr,
15+
Gen.asciiStr,
16+
Gen.choose(0L, 1L << 10).map(Duration(_, "millis"))
17+
)
18+
.map { case (a, b, c) => State.Compiled(a, b, c) }
19+
)
20+
21+
Gen.oneOf(
22+
Gen.const(State.Init),
23+
genWithText,
24+
genWithText.map(State.Compiling(_))
25+
)
26+
}
27+
28+
property("json encoding/decoding works") {
29+
Prop.forAll(genState) { state =>
30+
val str = State.stateToJsonString(state)
31+
assertEquals(
32+
State.stringToState(str),
33+
Right(state),
34+
s"encoded $state to $str"
35+
)
36+
}
37+
}
38+
}

project/Dependencies.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ object Dependencies {
1111
lazy val jawnParser = Def.setting("org.typelevel" %%% "jawn-parser" % "1.5.1")
1212
lazy val jawnAst = Def.setting("org.typelevel" %%% "jawn-ast" % "1.5.1")
1313
lazy val jython = Def.setting("org.python" % "jython-standalone" % "2.7.3")
14-
lazy val munit = Def.setting("org.scalameta" %% "munit" % "1.0.0-M10")
15-
lazy val munitScalaCheck = Def.setting("org.scalameta" %% "munit-scalacheck" % "1.0.0-M10")
14+
lazy val munit = Def.setting("org.scalameta" %%% "munit" % "1.0.0-M10")
15+
lazy val munitScalaCheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % "1.0.0-M10")
1616
lazy val paiges = Def.setting("org.typelevel" %%% "paiges-core" % "0.4.3")
1717
lazy val scalaCheck =
1818
Def.setting("org.scalacheck" %%% "scalacheck" % "1.17.0")

0 commit comments

Comments
 (0)