Browse Source

many changes

development
Rohan Sircar 3 years ago
parent
commit
08b320b051
  1. 3
      assets/app/index.html
  2. 26
      build.sbt
  3. 4
      src/main/scala/outwatch/util/MonixWebSocket.scala
  4. 4
      src/main/scala/outwatch/util/WebSocket2.scala
  5. 143
      src/main/scala/outwatchapp/MainApp.scala
  6. 0
      src/main/scala/outwatchapp/components/Chartjs.scala
  7. 33
      src/main/scala/outwatchapp/components/ChartjsDemo.scala
  8. 141
      src/main/scala/outwatchapp/components/DatatablesDemo.scala
  9. 492
      src/main/scala/outwatchapp/components/ExampleDataTable.scala
  10. 2
      src/main/scala/outwatchapp/components/Fusejs.scala
  11. 6
      src/main/scala/outwatchapp/components/RequestDemo.scala
  12. 104
      src/main/scala/outwatchapp/components/SweetAlertDemo.scala
  13. 30
      src/main/scala/outwatchapp/implicits/package.scala
  14. 65
      src/main/scala/outwatchapp/pages/HomePage.scala
  15. 34
      src/main/scala/outwatchapp/util/ParallelDemo.scala
  16. 14
      src/main/scala/outwatchapp/util/SubtleCryptoTest.scala
  17. 44
      src/main/scala/outwatchapp/util/WorkerTest.scala
  18. 77
      src/main/scala/outwatchapp/util/reactive/DedicatedWorker.scala
  19. 16
      src/main/scala/outwatchapp/util/reactive/Exceptions.scala
  20. 165
      src/main/scala/outwatchapp/util/reactive/ReconnectingWebSocket.scala
  21. 94
      src/main/scala/outwatchapp/util/reactive/WebSocket.scala
  22. 111
      src/main/scala/outwatchapp/util/reactive/WebWorker.scala
  23. 1
      src/main/scala/outwatchapp/util/reactive/package.scala
  24. 23690
      src/main/scala/outwatchapp/util/reactive/store/test.css
  25. 67
      yarn.lock

3
assets/app/index.html

@ -6,6 +6,9 @@
<title>Outwatch App</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/blk-design-system@1.0.2/assets/css/nucleo-icons.css" integrity="sha256-03+9B37/His+rzjhgA6Y1+ByU9DGN2ZPWjjA5CJJF2w=" crossorigin="anonymous">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script> -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@3/dark.css"> -->
</head>

26
build.sbt

@ -35,18 +35,28 @@ libraryDependencies ++= Seq(
"com.beachape" %%% "enumeratum-circe" % "1.6.1",
"com.github.valskalla" %%% "odin-core" % "0.7.0+95-ab4381ae+20201227-1831-SNAPSHOT",
"io.github.cquiroz" %%% "scala-java-time" % "2.1.0",
"io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.1.0"
"io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.1.0",
"com.lihaoyi" %%% "scalatags" % "0.9.2"
// "com.clovellytech" %%% "outwatch-router" % "0.0.9+7-5be0b1a2+20201227-2019-SNAPSHOT"
)
Compile / npmDependencies ++= Seq(
"jquery" -> "3.3.1",
"jquery" -> "3.5.1",
"@types/jquery" -> "3.5.5",
"blk-design-system" -> "1.0.2",
"bootstrap" -> "4.5.3",
"@types/chart.js" -> "2.9.11",
"chart.js" -> "2.9.3",
"snabbdom" -> "git://github.com/outwatch/snabbdom.git#semver:0.7.5",
"fuse.js" -> "6.4.3"
"fuse.js" -> "6.4.3",
"datatables.net-bs4" -> "1.10.23",
// "datatables.net-dt" -> "1.10.23",
"@types/datatables.net" -> "1.10.19",
"sweetalert2" -> "10.12.6",
"@sweetalert2/themes" -> "4.0.1",
"reconnecting-websocket" -> "4.4.0",
"paralleljs" -> "1.1.0",
"@types/paralleljs" -> "0.0.20"
)
Compile / npmDevDependencies ++= Seq(
@ -57,8 +67,14 @@ Compile / npmDevDependencies ++= Seq(
"url-loader" -> "1.1.2"
)
stIgnore ++= List("jquery", "blk-design-system", "bootstrap")
stIgnore ++= List("snabbdom")
stIgnore ++= List(
"datatables.net-bs4",
"datatables.net-dt",
"blk-design-system",
"bootstrap",
"snabbdom",
"@sweetalert2/themes"
)
stStdlib := List("es6")
stUseScalaJsDom := false

4
src/main/scala/outwatch/util/MonixWebSocket.scala

@ -25,7 +25,7 @@ object MonixWS {
class MonixWS[F[_], S](val url: String)(implicit F: Sync[F], S: Show[S]) {
val ws = new org.scalajs.dom.WebSocket(url)
lazy val source: Observable[MessageEvent] =
val source: Observable[MessageEvent] =
Observable.create[MessageEvent](OverflowStrategy.Unbounded) { sub =>
ws.onmessage = (e: MessageEvent) => sub.onNext(e)
ws.onerror =
@ -33,7 +33,7 @@ class MonixWS[F[_], S](val url: String)(implicit F: Sync[F], S: Show[S]) {
Cancelable(() => ws.close())
}
lazy val sink: F[Observer[S]] = {
val sink: F[Observer[S]] = {
F.delay {
new Observer[S] {
override def onNext(elem: S): Future[Ack] = {

4
src/main/scala/outwatch/util/WebSocket2.scala

@ -20,7 +20,7 @@ object WebSocketF {
class WebSocketF[F[_]](val url: String)(implicit F: Sync[F]) {
val ws = new org.scalajs.dom.WebSocket(url)
lazy val source: Observable[MessageEvent] =
val source: Observable[MessageEvent] =
Observable.create[MessageEvent] { observer =>
ws.onmessage = (e: MessageEvent) => observer.onNext(e)
ws.onerror =
@ -28,7 +28,7 @@ class WebSocketF[F[_]](val url: String)(implicit F: Sync[F]) {
Cancelable(() => ws.close())
}
lazy val sink: F[Observer[String]] = {
val sink: F[Observer[String]] = {
F.delay {
new Observer[String] {
override def onNext(elem: String): Unit = ws.send(elem)

143
src/main/scala/outwatchapp/MainApp.scala

@ -8,28 +8,47 @@ import io.odin.consoleLogger
import monix.bio.Task
import monix.eval.Coeval
import monix.reactive.Observable
import monix.{eval => me}
import org.scalajs.dom.raw.Element
import outwatch._
import outwatch.dsl._
import outwatch.router._
import outwatchapp.components.ChartjsDemo
import outwatchapp.components.CounterDemo
import outwatchapp.components.DatatablesDemo
import outwatchapp.components.FusejsDemo
import outwatchapp.components.RequestDemo
import outwatchapp.components.todo.ChartjsDemo
import outwatchapp.components.todo.FusejsDemo
import outwatchapp.pages.HomePage
import outwatchapp.ui.components.todo.TodoListStore
import outwatchapp.util.IOUtils
import outwatchapp.util.reactive.WebSocket
import outwatchapp.util.reactive.WebWorker
import outwatchapp.util.reactive.WebsocketData
import outwatchapp.util.reactive.WorkerData
import outwatchapp.util.reactive.ReconnectingWebSocket
import cats.syntax.eq._
import monix.execution.Ack
import monix.reactive.Observer
class MainApp(el: Element)(implicit
backend: AppTypes.Backend,
store: RouterStore[Page]
) {
import MainApp._
def run: Task[Unit] = for {
resolver <- resolver
_ <- OutWatch.renderInto[Task](el, Router(resolver))
} yield ()
def producer(data: Long, ws: Observer[WebsocketData]): me.Task[Unit] =
me.Task.deferFuture(ws.onNext(WebsocketData(data))).flatMap {
case Ack.Continue => producer(data + 1, ws)
// .delayExecution(3.seconds)
case Ack.Stop => me.Task.unit
}
def resolver: Task[PartialFunction[Page, VDomModifier]] =
Task.deferAction(implicit s =>
for {
@ -38,35 +57,131 @@ class MainApp(el: Element)(implicit
todoStore <- TodoListStore(consoleLogger[Task]())
requestDemo <- RequestDemo(todoStore)
demoWorker <- WebWorker[WorkerData]("/worker.js")
dtDemo <- DatatablesDemo()
_ <- IOUtils
.toIO(
Observable
.interval(1.second)
.take(2)
.doOnNextF(i => Coeval(println(s"Producer emitted $i")))
.doOnNextF(i =>
me.Task.deferFuture(demoWorker.onNext(WorkerData(i))).void
)
.completedL >>
me.Task.deferFuture(demoWorker.onNext(WorkerData(999)))
// .delayExecution(6.seconds)
)
.startAndForget
_ <-
ReconnectingWebSocket[WebsocketData]("ws://localhost:6789").flatMap {
case (source, sink) =>
IOUtils
.toIO(
me.Task.parZip2(
// Observable
// .interval(3.seconds)
// .doOnNext(_ => me.Task(println("Sending websocket data")))
// .doOnNext(i =>
// me.Task.deferFuture(
// ws.onNext(WebsocketData(i))
// ) >> me.Task.unit
// )
// .completedL,
producer(0, sink.value),
source.value
.doOnNext(data => me.Task(println(s"Received : $data")))
.completedL
)
)
}
// .onErrorRestartIf {
// case e: Exception =>
// e.getMessage() === "Websocket closed"
// case _ => false
// }
// .onErrorRestartLoop(Backoff(3, 1.second)) { (err, state, retry) =>
// val Backoff(maxRetries, delay) = state
// if (maxRetries > 0)
// retry(Backoff(maxRetries - 1, delay * 2)).delayExecution(delay)
// else
// // No retries left, rethrow the error
// Task.raiseError(err)
// }
.startAndForget
// .start
// .startAndForget
// _ <- SweetAlertDemo.dumbNotif
// _ <- Task(println(s"Notif result: $notifRes"))
} yield {
case Page.Home => wire[HomePage].render
case Page.SomePage =>
div(
div(cls := "title", "SomePage"),
// RequestDemo(todoStore),
Observable
.interval(1.second)
.doOnNextF(i => Coeval(println(s"Producer emitted $i")))
.doOnNextF(i =>
Coeval(demoWorker.onNext(WorkerData(i))) >> Coeval.unit
)
.map(_ => div()),
// Observable
// .interval(1.second)
// .doOnNextF(i => Coeval(println(s"Producer emitted $i")))
// .doOnNextF(i =>
// Coeval(demoWorker.onNext(WorkerData(i))) >> Coeval.unit
// )
// .take(2)
// .map(_ => div()),
demoWorker.map(_.toString).map(v => p(cls := "text-white", v)),
requestDemo,
div(cls := "slider")
)
case Page.UserHome(id) =>
div(
cls := "text-white",
// cls := "text-white",
div(cls := "title", "UserHome"),
s"User id: $id",
div(FusejsDemo.y.map(_.item.toString).mkString(" "))
div(FusejsDemo.y.map(_.item.toString).mkString(" ")),
// ExampleDataTable.value(
// onDomMount.asHtml.foreachSync(el =>
// Coeval(DatatablesDemo.init(el)) >> Coeval.unit
// )
// )
dtDemo
)
case Page.NotFound =>
div(
div(cls := "title", "NotFound"),
p(cls := "text-white", "notfound")
Task(
div(
cls := "page-header error-page header-filter",
div(
cls := "page-header-image"
// style := js.Dictionary(
// "background-image" -> "url('../assets/img/braden-collum.jpg')"
// )
),
div(
cls := "container",
cls := "text-center",
div(
cls := "row",
div(
cls := "col-md-12",
h1(
fontSize := "12em",
color := "#fff",
letterSpacing := "14px",
fontWeight := 700,
cls := "title",
"404"
),
h2(cls := "description", "Page not found :("),
h4(
cls := "description",
"Ooooups! Looks like you got lost."
)
)
)
)
)
)
}
)
}
object MainApp {
final case class Backoff(maxRetries: Long, delay: FiniteDuration)
}

0
src/main/scala/outwatchapp/components/todo/Chartjs.scala → src/main/scala/outwatchapp/components/Chartjs.scala

33
src/main/scala/outwatchapp/components/todo/ChartjsDemo.scala → src/main/scala/outwatchapp/components/ChartjsDemo.scala

@ -1,4 +1,4 @@
package outwatchapp.components.todo
package outwatchapp.components
import scala.scalajs.js.|
import scala.util.Random
@ -6,9 +6,10 @@ import colibri.ext.monix._
import com.softwaremill.tagging._
import monix.bio.Task
import monix.eval.Coeval
import monix.execution.Cancelable
import outwatch.HtmlVNode
import outwatch.ManagedSubscriptions.managedElement
import outwatch.dsl._
import outwatch.reactive.handlers.monix._
import typings.chartJs.mod._
import typings.chartJs.mod.{^ => Chart}
import typings.moment.mod.Moment
@ -20,29 +21,19 @@ import scalajs.js
sealed trait ChartjsDemo
object ChartjsDemo {
val random = new Random()
def apply(): Task[HtmlVNode @@ ChartjsDemo] = Task.deferAction(implicit s =>
for {
canvasH <- Handler.createF[Task, HTMLCanvasElement]
} yield div(
def apply(): Task[HtmlVNode @@ ChartjsDemo] = Coeval(
div(
canvas(
onDomMount.asHtml.map(a =>
a.asInstanceOf[HTMLCanvasElement]
) --> canvasH
),
div(
canvasH
.doOnNextF(el =>
Coeval(
new Chart(
el,
chartConfig(ChartType.bar, randomData(100, random.nextInt()))
)
) >> Coeval.unit
managedElement { el =>
val chart = new Chart(
el.asInstanceOf[HTMLCanvasElement],
chartConfig(ChartType.bar, randomData(100, random.nextInt()))
)
.map(_ => div())
Cancelable(() => chart.destroy())
}
)
).taggedWith[ChartjsDemo]
)
).to[Task]
def randomData(
max: Int,

141
src/main/scala/outwatchapp/components/DatatablesDemo.scala

@ -0,0 +1,141 @@
package outwatchapp.components
import scala.annotation.unused
import scala.scalajs.js.annotation.JSExportAll
import scala.scalajs.js.annotation.JSImport
import colibri.ext.monix._
import monix.bio.Task
import monix.eval.Coeval
import monix.execution.Cancelable
import org.scalajs.dom.raw.HTMLElement
import outwatch.ManagedSubscriptions.managedElement
import typings.datatablesNet.DataTables.ColumnSettings
import typings.datatablesNet.DataTables.FunctionColumnRender
import typings.datatablesNet.DataTables.Settings
import typings.datatablesNet.datatablesNetRequire
import typings.jquery.JQuery_
import typings.jquery.{mod => $}
import scalajs.js
@js.native
@JSImport(
"datatables.net-bs4/css/dataTables.bootstrap4.min.css",
JSImport.Namespace
)
object DataTablesBs4Css extends js.Object
@js.native
@JSImport(
"datatables.net-bs4/js/dataTables.bootstrap4.min.js",
JSImport.Namespace
)
object DataTablesBs4Js extends js.Object
object DatatablesDemo {
implicit def isJQueryDT[T](x: JQuery_[T]): typings.datatablesNet.JQuery =
x.asInstanceOf[typings.datatablesNet.JQuery]
datatablesNetRequire
DataTablesBs4Css
DataTablesBs4Js
@unused
private def _test(tableElement: HTMLElement) = $(tableElement)
.DataTable(
Settings().setColumnsVarargs(
ColumnSettings().setData("name").setRender(nameRenderFn)
)
)
.destroy()
val nameRenderFn: FunctionColumnRender = (_data, tpe, row, d) => {
if (tpe.toString == "display") println("Matched")
else println("Did not match")
// this can be done with scalatags instead
s"""
<div>${_data.toString.toUpperCase}</div>
"""
// with scalatags
// import scalatags.Text.all._
// div(_data.toString.toUpperCase).render
}
@JSExportAll
case class TestData(
name: String,
position: String,
location: String,
id: String,
date: String,
salary: String
)
val dataset = js
.Array(
TestData(
"Tiger Nixon",
"System Architect",
"Edinburgh",
"5421",
"2011/04/25",
"$320,800"
),
TestData(
"Garrett Winters",
"Accountant",
"Tokyo",
"8422",
"2011/07/25",
"$170,750"
),
TestData(
"Ashton Cox",
"Junior Technical Author",
"San Francisco",
"1562",
"2009/01/12",
"$86,000"
),
TestData(
"Cedric Kelly",
"Senior Javascript Developer",
"Edinburgh",
"6224",
"2012/03/29",
"$433,060"
)
)
// $(document).ready(_ => $(tableElement).DataTable())
def apply() = {
import outwatch.dsl._
Coeval(
div(
table(idAttr := "myTable")(managedElement { el =>
val dt = $(el).DataTable(
Settings()
.setColumnsVarargs(
ColumnSettings()
.setTitle("Name")
.setData("name")
.setRender(nameRenderFn),
ColumnSettings().setData("position"),
ColumnSettings().setData("location"),
ColumnSettings().setData("id"),
ColumnSettings().setData("date"),
ColumnSettings().setData("salary")
)
.setDom("Blfrtip")
.setData(dataset)
)
Cancelable { () =>
println("Destroying")
dt.destroy()
}
})
)
).to[Task]
}
}

492
src/main/scala/outwatchapp/components/ExampleDataTable.scala

@ -0,0 +1,492 @@
package outwatchapp.components
import outwatch.dsl._
object Table {
val value = table(
idAttr := "myTable",
cls := "display",
cls := "table",
// style := "width: 100%",
width := "100%",
// style("width") := "100%",
thead(
tr(
th("Name"),
th("Position"),
th("Office"),
th("Age"),
th("Start date"),
th("Salary")
)
),
tbody(
tr(
td("Tiger Nixon"),
td("System Architect"),
td("Edinburgh"),
td("61"),
td("2011/04/25"),
td("$320,800")
),
tr(
td("Garrett Winters"),
td("Accountant"),
td("Tokyo"),
td("63"),
td("2011/07/25"),
td("$170,750")
),
tr(
td("Ashton Cox"),
td("Junior Technical Author"),
td("San Francisco"),
td("66"),
td("2009/01/12"),
td("$86,000")
),
tr(
td("Cedric Kelly"),
td("Senior Javascript Developer"),
td("Edinburgh"),
td("22"),
td("2012/03/29"),
td("$433,060")
),
tr(
td("Airi Satou"),
td("Accountant"),
td("Tokyo"),
td("33"),
td("2008/11/28"),
td("$162,700")
),
tr(
td("Brielle Williamson"),
td("Integration Specialist"),
td("New York"),
td("61"),
td("2012/12/02"),
td("$372,000")
),
tr(
td("Herrod Chandler"),
td("Sales Assistant"),
td("San Francisco"),
td("59"),
td("2012/08/06"),
td("$137,500")
),
tr(
td("Rhona Davidson"),
td("Integration Specialist"),
td("Tokyo"),
td("55"),
td("2010/10/14"),
td("$327,900")
),
tr(
td("Colleen Hurst"),
td("Javascript Developer"),
td("San Francisco"),
td("39"),
td("2009/09/15"),
td("$205,500")
),
tr(
td("Sonya Frost"),
td("Software Engineer"),
td("Edinburgh"),
td("23"),
td("2008/12/13"),
td("$103,600")
),
tr(
td("Jena Gaines"),
td("Office Manager"),
td("London"),
td("30"),
td("2008/12/19"),
td("$90,560")
),
tr(
td("Quinn Flynn"),
td("Support Lead"),
td("Edinburgh"),
td("22"),
td("2013/03/03"),
td("$342,000")
),
tr(
td("Charde Marshall"),
td("Regional Director"),
td("San Francisco"),
td("36"),
td("2008/10/16"),
td("$470,600")
),
tr(
td("Haley Kennedy"),
td("Senior Marketing Designer"),
td("London"),
td("43"),
td("2012/12/18"),
td("$313,500")
),
tr(
td("Tatyana Fitzpatrick"),
td("Regional Director"),
td("London"),
td("19"),
td("2010/03/17"),
td("$385,750")
),
tr(
td("Michael Silva"),
td("Marketing Designer"),
td("London"),
td("66"),
td("2012/11/27"),
td("$198,500")
),
tr(
td("Paul Byrd"),
td("Chief Financial Officer (CFO)"),
td("New York"),
td("64"),
td("2010/06/09"),
td("$725,000")
),
tr(
td("Gloria Little"),
td("Systems Administrator"),
td("New York"),
td("59"),
td("2009/04/10"),
td("$237,500")
),
tr(
td("Bradley Greer"),
td("Software Engineer"),
td("London"),
td("41"),
td("2012/10/13"),
td("$132,000")
),
tr(
td("Dai Rios"),
td("Personnel Lead"),
td("Edinburgh"),
td("35"),
td("2012/09/26"),
td("$217,500")
),
tr(
td("Jenette Caldwell"),
td("Development Lead"),
td("New York"),
td("30"),
td("2011/09/03"),
td("$345,000")
),
tr(
td("Yuri Berry"),
td("Chief Marketing Officer (CMO)"),
td("New York"),
td("40"),
td("2009/06/25"),
td("$675,000")
),
tr(
td("Caesar Vance"),
td("Pre-Sales Support"),
td("New York"),
td("21"),
td("2011/12/12"),
td("$106,450")
),
tr(
td("Doris Wilder"),
td("Sales Assistant"),
td("Sydney"),
td("23"),
td("2010/09/20"),
td("$85,600")
),
tr(
td("Angelica Ramos"),
td("Chief Executive Officer (CEO)"),
td("London"),
td("47"),
td("2009/10/09"),
td("$1,200,000")
),
tr(
td("Gavin Joyce"),
td("Developer"),
td("Edinburgh"),
td("42"),
td("2010/12/22"),
td("$92,575")
),
tr(
td("Jennifer Chang"),
td("Regional Director"),
td("Singapore"),
td("28"),
td("2010/11/14"),
td("$357,650")
),
tr(
td("Brenden Wagner"),
td("Software Engineer"),
td("San Francisco"),
td("28"),
td("2011/06/07"),
td("$206,850")
),
tr(
td("Fiona Green"),
td("Chief Operating Officer (COO)"),
td("San Francisco"),
td("48"),
td("2010/03/11"),
td("$850,000")
),
tr(
td("Shou Itou"),
td("Regional Marketing"),
td("Tokyo"),
td("20"),
td("2011/08/14"),
td("$163,000")
),
tr(
td("Michelle House"),
td("Integration Specialist"),
td("Sydney"),
td("37"),
td("2011/06/02"),
td("$95,400")
),
tr(
td("Suki Burks"),
td("Developer"),
td("London"),
td("53"),
td("2009/10/22"),
td("$114,500")
),
tr(
td("Prescott Bartlett"),
td("Technical Author"),
td("London"),
td("27"),
td("2011/05/07"),
td("$145,000")
),
tr(
td("Gavin Cortez"),
td("Team Leader"),
td("San Francisco"),
td("22"),
td("2008/10/26"),
td("$235,500")
),
tr(
td("Martena Mccray"),
td("Post-Sales support"),
td("Edinburgh"),
td("46"),
td("2011/03/09"),
td("$324,050")
),
tr(
td("Unity Butler"),
td("Marketing Designer"),
td("San Francisco"),
td("47"),
td("2009/12/09"),
td("$85,675")
),
tr(
td("Howard Hatfield"),
td("Office Manager"),
td("San Francisco"),
td("51"),
td("2008/12/16"),
td("$164,500")
),
tr(
td("Hope Fuentes"),
td("Secretary"),
td("San Francisco"),
td("41"),
td("2010/02/12"),
td("$109,850")
),
tr(
td("Vivian Harrell"),
td("Financial Controller"),
td("San Francisco"),
td("62"),
td("2009/02/14"),
td("$452,500")
),
tr(
td("Timothy Mooney"),
td("Office Manager"),
td("London"),
td("37"),
td("2008/12/11"),
td("$136,200")
),
tr(
td("Jackson Bradshaw"),
td("Director"),
td("New York"),
td("65"),
td("2008/09/26"),
td("$645,750")
),
tr(
td("Olivia Liang"),
td("Support Engineer"),
td("Singapore"),
td("64"),
td("2011/02/03"),
td("$234,500")
),
tr(
td("Bruno Nash"),
td("Software Engineer"),
td("London"),
td("38"),
td("2011/05/03"),
td("$163,500")
),
tr(
td("Sakura Yamamoto"),
td("Support Engineer"),
td("Tokyo"),
td("37"),
td("2009/08/19"),
td("$139,575")
),
tr(
td("Thor Walton"),
td("Developer"),
td("New York"),
td("61"),
td("2013/08/11"),
td("$98,540")
),
tr(
td("Finn Camacho"),
td("Support Engineer"),
td("San Francisco"),
td("47"),
td("2009/07/07"),
td("$87,500")
),
tr(
td("Serge Baldwin"),
td("Data Coordinator"),
td("Singapore"),
td("64"),
td("2012/04/09"),
td("$138,575")
),
tr(
td("Zenaida Frank"),
td("Software Engineer"),
td("New York"),
td("63"),
td("2010/01/04"),
td("$125,250")
),
tr(
td("Zorita Serrano"),
td("Software Engineer"),
td("San Francisco"),
td("56"),
td("2012/06/01"),
td("$115,000")
),
tr(
td("Jennifer Acosta"),
td("Junior Javascript Developer"),
td("Edinburgh"),
td("43"),
td("2013/02/01"),
td("$75,650")
),
tr(
td("Cara Stevens"),
td("Sales Assistant"),
td("New York"),
td("46"),
td("2011/12/06"),
td("$145,600")
),
tr(
td("Hermione Butler"),
td("Regional Director"),
td("London"),
td("47"),
td("2011/03/21"),
td("$356,250")
),
tr(
td("Lael Greer"),
td("Systems Administrator"),
td("London"),
td("21"),
td("2009/02/27"),
td("$103,500")
),
tr(
td("Jonas Alexander"),
td("Developer"),
td("San Francisco"),
td("30"),
td("2010/07/14"),
td("$86,500")
),
tr(
td("Shad Decker"),
td("Regional Director"),
td("Edinburgh"),
td("51"),
td("2008/11/13"),
td("$183,000")
),
tr(
td("Michael Bruce"),
td("Javascript Developer"),
td("Singapore"),
td("29"),
td("2011/06/27"),
td("$183,000")
),
tr(
td("Donna Snider"),
td("Customer Support"),
td("New York"),
td("27"),
td("2011/01/25"),
td("$112,000")
)
),
tfoot(
tr(
th("Name"),
th("Position"),
th("Office"),
th("Age"),
th("Start date"),
th("Salary")
)
)
)
}

2
src/main/scala/outwatchapp/components/todo/Fusejs.scala → src/main/scala/outwatchapp/components/Fusejs.scala

@ -1,4 +1,4 @@
package outwatchapp.components.todo
package outwatchapp.components
import outwatchapp.ui.components.todo.Todo
import typings.fuseJs.mod.Fuse.IFuseOptions

6
src/main/scala/outwatchapp/components/RequestDemo.scala

@ -5,14 +5,14 @@ import com.softwaremill.tagging._
import monix.bio._
import monix.eval.Coeval
import monix.{eval => me}
import outwatchapp.ui.components.todo.Todo
import outwatchapp.ui.components.todo.TodoListStore
import outwatchapp.util.reactive.store.Store
import outwatch._
import outwatch.dsl._
import outwatch.reactive.handlers.monix._
import outwatchapp.AppTypes
import outwatchapp.implicits._
import outwatchapp.ui.components.todo.Todo
import outwatchapp.ui.components.todo.TodoListStore
import outwatchapp.util.reactive.store.Store
import sttp.client._
sealed trait RequestDemo

104
src/main/scala/outwatchapp/components/SweetAlertDemo.scala

@ -0,0 +1,104 @@
package outwatchapp.components
import scala.scalajs.js.annotation.JSImport
import monix.bio.Task
import outwatchapp.implicits._
import typings.sweetalert2.mod.SweetAlertIcon
import typings.sweetalert2.mod.SweetAlertOptions
import typings.sweetalert2.mod.SweetAlertPosition
import scalajs.js
object SweetAlertDemo {
@JSImport(
"@sweetalert2/themes/borderless/borderless.min.css",
JSImport.Namespace
)
@js.native
object SweetAlertBorderlessTheme extends js.Object
@JSImport("@sweetalert2/themes/dark/dark.min.css", JSImport.Namespace)
@js.native
object SweetAlertDarkTheme extends js.Object
@JSImport("sweetalert2/dist/sweetalert2.js", JSImport.Namespace)
@js.native
object SweetAlertJs extends js.Object
SweetAlertDarkTheme
val Swal = SweetAlertJs.asInstanceOf[typings.sweetalert2.mod.default.type]
val loginPrompt = {
import scalatags.JsDom.all._
val nameInput = input(
tpe := "text",
`id` := "loginName",
cls := "swal-input",
placeholder := "Username"
).render
val passInput = input(
tpe := "password",
`id` := "loginPass",
cls := "swal-input",
placeholder := "Password"
).render
val html = div(nameInput, passInput).render.asST
def nameAndPass(a: js.Any) = (nameInput.value, passInput.value)
Swal.fireL(
SweetAlertOptions()
.setTitle("Login Form")
.setHtml(html)
.setFocusConfirm(false)
.setConfirmButtonText("Sign In")
.setPreConfirm(nameAndPass _)
)
}
val successPrompt =
Swal.fireL(
SweetAlertOptions()
.setPosition(SweetAlertPosition.`top-end`)
.setIcon(SweetAlertIcon.success)
.setTitle("Login Success")
.setShowConfirmButton(false)
.setTimer(1500)
) >> Task.unit
val failurePrompt =
Swal.fireL(
SweetAlertOptions()
.setPosition(SweetAlertPosition.`top-end`)
.setIcon(SweetAlertIcon.error)
.setTitle("Login Failure")
.setShowConfirmButton(false)
.setTimer(1500)
) >> Task.unit
val dumbNotif =
Swal
.fireL(
SweetAlertOptions()
.setTitle("Hello there")
.setText("Welcome to outwatch app")
// .setInput(SweetAlertInput.text)
// .setShowCancelButton(true)
.setIcon(SweetAlertIcon.info)
// .setToast(true)
// .setPreConfirm { res: String =>
// val username = Swal.getPopup().asOption
// username match {
// case Some(value) =>
// println(s"Got value: $value")
// 1
// case None =>
// println("Got none")
// 2
// }
// res
// }
) >> Task.unit
// .map(_.value.toOption)
}

30
src/main/scala/outwatchapp/implicits/package.scala

@ -1,11 +1,19 @@
package outwatchapp
import scala.util.Try
import colibri.LiftSink
import colibri.LiftSource
import colibri.ext.monix._
import monix.bio.Task
import monix.execution.Scheduler
import monix.reactive.Observable
import monix.reactive.Observer
import org.scalajs.dom.raw.HTMLElement
import typings.sweetalert2.mod.SweetAlertOptions
import typings.sweetalert2.mod.{default => Swal}
import scalajs.js.|
package object implicits {
// implicit def sinkForMyMonixProSub[A, M] =
@ -20,10 +28,26 @@ package object implicits {
// )
// }
implicit class Ops[A](val sink: Observer[A]) {
implicit class Ops[A](val sink: Observer[A]) extends AnyVal {
@inline def liftSink[G[_]: LiftSink]: G[A] = LiftSink[G].lift(sink)
}
implicit class Ops2[A](val source: Observable[A])(implicit s: Scheduler) {
@inline def liftSource[G[_]: LiftSource]: G[A] = LiftSource[G].lift(source)
implicit class Ops2[A](val source: Observable[A]) extends AnyVal {
@inline def liftSource[G[_]: LiftSource](implicit s: Scheduler): G[A] =
LiftSource[G].lift(source)
}
implicit class HTMLElementOps[T <: HTMLElement](private val el: T)
extends AnyVal {
def asST = el.asInstanceOf[typings.std.HTMLElement]
}
implicit class SweetAlertOps(private val sw: Swal.type) extends AnyVal {
def fireL[T](options: SweetAlertOptions[T, _]) =
Task.deferFuture(sw.fire(options).toFuture)
}
implicit class NullUnionOps[A, B](private val union: A | B) extends AnyVal {
@scala.inline
def asOption(implicit ev: B =:= scala.Null) = Try(
union.asInstanceOf[A]
).toOption
}
}

65
src/main/scala/outwatchapp/pages/HomePage.scala

@ -1,36 +1,59 @@
package outwatchapp.pages
import cats.syntax.eq._
import com.softwaremill.tagging._
import monix.bio.Task
import outwatch._
import outwatch.dsl._
import outwatchapp.components.ChartjsDemo
import outwatchapp.components.CounterDemo
import outwatchapp.components.todo.ChartjsDemo
import outwatchapp.components.SweetAlertDemo
class HomePage(
counterDemo: VNode @@ CounterDemo,
chartDemo: VNode @@ ChartjsDemo
) {
def render = div(
div(cls := "title", "Home"),
div(
cls := "card",
val loginDemo = for {
res <- SweetAlertDemo.loginPrompt.map(_.value.toOption)
_ <- Task(println(s"Got $res"))
_ <-
if (res === Some("foo" -> "bar")) SweetAlertDemo.successPrompt
else SweetAlertDemo.failurePrompt
} yield ()
def render = Task.deferAction(implicit s =>
Task(
div(
cls := "card-body",
counterDemo,
chartDemo,
p(
cls := "text-white",
div(cls := "title", "Home"),
div(
cls := "card",
div(
"hm",
htmlTag("blockQuote")(
cls := "blockquote",
p(
cls := "mb-0",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante."
),
footer(
cls := "blockquote-footer",
"Someone famous in ",
cite(title := "Source Title", "Source Title")
cls := "card-body",
counterDemo,
chartDemo,
div(
cls := "text-center",
button(
cls := "btn btn-primary",
onClick.preventDefault.doAsync(loginDemo),
"Login"
)
),
p(
cls := "text-white",
div(
"hm",
htmlTag("blockQuote")(
cls := "blockquote",
p(
cls := "mb-0",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante."
),
footer(
cls := "blockquote-footer",
"Someone famous in ",
cite(title := "Source Title", "Source Title")
)
)
)
)
)

34
src/main/scala/outwatchapp/util/ParallelDemo.scala

@ -0,0 +1,34 @@
package outwatchapp.util
import scala.concurrent.Future
import typings.paralleljs.ParallelOptions
import typings.paralleljs.mod.{^ => Parallel}
import scalajs.js
import scalajs.js.|
object ParallelDemo {
val p =
new Parallel(js.Array(1, 2, 3, 4, 5), ParallelOptions().setMaxWorkers(2))
// val pr = new js.Promise()
// p.map((_: Int) * 2)
def toFuture[A](p: Parallel[A]): Future[A] = {
val p2 = scala.concurrent.Promise[A]()
p.`then`(
{ (v: A) =>
p2.success(v)
(): Unit | js.Thenable[Unit]
},
{ (e: typings.std.Error) =>
p2.failure(e match {
case th: Throwable => th
case _ => js.JavaScriptException(e)
})
(): Unit | js.Thenable[Unit]
}
)
p2.future
}
}

14
src/main/scala/outwatchapp/util/SubtleCryptoTest.scala

@ -0,0 +1,14 @@
package outwatchapp.util
// import typings.std.{SubtleCrypto => TS}
// import org.scalajs.dom.crypto.SubtleCrypto
import typings.std.global.Uint8Array
import typings.std.global.{SubtleCrypto => TS}
object SubtleCryptoTest {
val sc = new TS()
// Algorithm
// sc.deriveKey
// val n = WindowBase64
new Uint8Array(10)
}

44
src/main/scala/outwatchapp/util/WorkerTest.scala

@ -0,0 +1,44 @@
// package outwatchapp.util
// import org.scalajs.dom
// import scala.scalajs.js
// import scala.scalajs.js.annotation.JSExport
// import org.scalajs.dom.webworkers.DedicatedWorkerGlobalScope
// import scala.scalajs.js.annotation.JSGlobalScope
// import scala.scalajs.js.annotation.JSExportTopLevel
// // @js.native
// // object WorkerGlobal extends js.GlobalScope {
// // def addEventListener(`type`: String, f: js.Function): Unit = js.native
// // def postMessage(data: js.Any): Unit = js.native
// // }
// @js.native
// @JSGlobalScope
// object WorkerGlobal extends DedicatedWorkerGlobalScope
// @JSExportTopLevel("WorkerMain")
// object WorkerMain {
// @JSExport
// def main(): Unit = {
// // WorkerGlobal.addEventListener("message", onMessage _)
// WorkerGlobal.onmessage = onMessage _
// WorkerGlobal.postMessage(s"Started")
// }
// val timeMessage = """Time.*""".r
// var count = 0
// def onMessage(msg: dom.MessageEvent) = {
// val s = msg.data.asInstanceOf[String]
// s match {
// case timeMessage() =>
// count += 1
// if (count % 600 == 0)
// WorkerGlobal.postMessage("60fps")
// case _ =>
// WorkerGlobal.postMessage(s"Received: $s")
// }
// }
// }

77
src/main/scala/outwatchapp/util/reactive/DedicatedWorker.scala

@ -0,0 +1,77 @@
package outwatchapp.util.reactive
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Printer
import io.circe.parser._
import io.circe.syntax._
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import org.scalajs.dom.raw.MessageEvent
import org.scalajs.dom.webworkers.DedicatedWorkerGlobalScope
import scala.concurrent.Future
import outwatchapp.util.reactive.Exceptions.WrongTypeException
// @js.native
// @JSGlobalScope
// object WorkerGlobal extends DedicatedWorkerGlobalScope
// class DedicatedWorkerImpl[T: Encoder: Decoder](wg: DedicatedWorkerGlobalScope) {
// def run = Task.deferAction(implicit s =>
// for {
// _ <- Task.unit
// sub <- Task(ConcurrentSubject.publish[T])
// } yield ()
// )
// }
object DedicatedWorker {
def source[T: Decoder](wg: DedicatedWorkerGlobalScope) =
Observable.create[T](OverflowStrategy.DropOld(50)) { sub =>
val c = SingleAssignCancelable()
def onmessageFn(event: MessageEvent): Unit = {
event.data match {
case s: String =>
decode[T](s)
.map { res =>
if (sub.onNext(res) == Ack.Stop) c.cancel()
res
}
.left
.foreach(err =>
sub.onError(
new Exception(s"Failed to decode $s. Error was $err")
)
)
case other =>
sub.onError(WrongTypeException(s"Received wrong type: $other"))
}
}
wg.onmessage = onmessageFn _
c := Cancelable(() => wg.onmessage = _ => ())
}
def sink[T: Encoder](wg: DedicatedWorkerGlobalScope) =
new Observer[T] {
val printer = Printer.noSpaces
override def onNext(elem: T): Future[Ack] = {
// wg.onoffline
wg.postMessage(printer.print(elem.asJson))
Ack.Continue
}
override def onError(ex: Throwable): Unit = ex.printStackTrace()
override def onComplete(): Unit = println("Worker observer completed")
}
def apply[T: Encoder: Decoder](
wg: DedicatedWorkerGlobalScope
): DedicatedWorker[T] = MonixProSubject.from(sink(wg), source(wg))
}

16
src/main/scala/outwatchapp/util/reactive/Exceptions.scala

@ -0,0 +1,16 @@
package outwatchapp.util.reactive
object Exceptions {
//cause: Option[Throwable]) extends Exception(message, cause.orNull)
sealed abstract class ReactiveException(val message: String)
extends Throwable(message)
final case class DecodeException(override val message: String)
extends ReactiveException(message)
final case class WrongTypeException(override val message: String)
extends ReactiveException(message)
final case class UseAfterClose(override val message: String)
extends ReactiveException(message)
final case class TerminatedException(override val message: String)
extends ReactiveException(message)
}

165
src/main/scala/outwatchapp/util/reactive/ReconnectingWebSocket.scala

@ -0,0 +1,165 @@
package outwatchapp.util.reactive
import scala.concurrent.Future
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Printer
import io.circe.parser._
import io.circe.syntax._
import monix.bio.Task
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.CancelablePromise
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import typings.reconnectingWebsocket.eventsMod.CloseEvent
import typings.reconnectingWebsocket.eventsMod.ErrorEvent
import typings.reconnectingWebsocket.eventsMod.Event
import typings.reconnectingWebsocket.mod.{default => RW}
import scalajs.js
import typings.std.MessageEvent
import typings.reconnectingWebsocket.mod.Options
import outwatchapp.util.reactive.Exceptions.DecodeException
import outwatchapp.util.reactive.Exceptions.WrongTypeException
import monix.reactive.observers.Subscriber
import monix.reactive.observers.BufferedSubscriber
import outwatchapp.util.reactive.Exceptions.UseAfterClose
import monix.bio.IO
class ReconnectingWebSocketImpl[T: Encoder: Decoder](
ws: RW,
overflowStrategy: OverflowStrategy.Synchronous[T]
) {
val printer = Printer.noSpaces
/** @throws ReactiveException
*/
val source: Task[Observable[T]] =
Task.deferAction(implicit s =>
for {
obs <- Task(
Observable
.create[T](overflowStrategy) { sub =>
val c = SingleAssignCancelable()
val onmessage: js.Function1[MessageEvent[_], Unit] =
_.data match {
case s: String =>
decode[T](s)
.map { res =>
if (sub.onNext(res) == Ack.Stop) c.cancel()
res
}
.left
.foreach(err =>
sub.onError(
DecodeException(
s"Failed to decode $s. Error was $err"
)
)
)
case other =>
sub.onError(
WrongTypeException(s"Received wrong type: $other")
)
}
val emptyMsgFn: js.Function1[MessageEvent[_], Unit] =
_ => println("message fn not initialized")
ws.onmessage = onmessage
val onclose: js.Function1[CloseEvent, Unit] = a => {
println(
s"Websocket closing - ${a.code} ${a.wasClean} ${a.reason}"
)
sub.onComplete()
}
ws.onclose = onclose
val onerror: js.Function1[ErrorEvent, Unit] = (e: Event) =>
sub.onError(new Exception(s"Error in WebSocket: $e"))
ws.onerror = onerror
c := Cancelable { () => ws.onmessage = emptyMsgFn }
}
.publish
.refCount
)
_ <- Task(obs.subscribe(Observer.empty))
} yield obs
)
val sink: Task[Observer[T]] =
Task.deferAction(implicit s =>
Task(
BufferedSubscriber(
new Subscriber[T] {
import monix.execution.FutureUtils
import scala.concurrent.duration._
override def scheduler = s
override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson)
if (ws.readyState == 2 || ws.readyState == 3)
Ack.Stop
else {
FutureUtils
.delayedResult(2.second)(ws.send(msg))
.flatMap(_ => Ack.Continue)
}
}
override def onError(ex: Throwable): Unit = s.reportFailure(ex)
override def onComplete(): Unit = println("CLOSING WEBSOCKET 2 ")
},
OverflowStrategy.Default
)
)
)
}
object ReconnectingWebSocket {
sealed trait Error
final case object Error extends Error
/** @throws ReactiveException
*/
final case class Source[T](value: Observable[T])
/** A buffered Observer
*
* @param value
*/
final case class Sink[T](value: Observer[T]) {
def send(t: T) =
IO.deferFuture(value.onNext(t)).flatMap {
case Ack.Continue => Task.unit
case Ack.Stop => IO.raiseError(UseAfterClose("Websocket was closed"))
}
}
private val defaultOptions = Options()
.setMinReconnectionDelay(5000)
.setMaxRetries(2)
.setMaxEnqueuedMessages(50)
def onopen[A](op: => A): js.Function1[Event, Unit] = _ => op
def apply[T <: Product: Encoder: Decoder](
path: String,
options: Options = defaultOptions,
overflowStrategy: OverflowStrategy.Synchronous[T] =
OverflowStrategy.DropOld(50)
) = for {
websocket <- Task(new RW(path, js.undefined, options))
p = CancelablePromise[Unit]()
_ <- Task(websocket.onopen = onopen(p.success(())))
_ <- Task.deferFuture(p.future)
_ <- Task(websocket.onopen = onopen(()))
impl = new ReconnectingWebSocketImpl[T](websocket, overflowStrategy)
source <- impl.source
sink <- impl.sink
// _ <- Task.deferAction(implicit s => Task(source.subscribe(sink)))
} yield Source(source) -> Sink(sink)
}

94
src/main/scala/outwatchapp/util/reactive/WebSocket.scala

@ -1,6 +1,7 @@
package outwatchapp.util.reactive
import org.scalajs.dom.{raw => sjsdr}
import scala.concurrent.Future
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Printer
@ -10,13 +11,20 @@ import io.circe.syntax._
import monix.bio.Task
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.CancelablePromise
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import outwatchapp.util.reactive.MonixProSubject
import org.scalajs.dom.raw.Event
import org.scalajs.dom.raw.MessageEvent
import scala.concurrent.Future
import org.scalajs.dom.{raw => sjsdr}
import outwatchapp.util.reactive.MonixProSubject
import monix.reactive.observers.Subscriber
import monix.execution.Scheduler
import monix.reactive.observers.BufferedSubscriber
import monix.execution.cancelables.CompositeCancelable
import scala.concurrent.duration._
class WebSocketImpl[T: Encoder: Decoder](
ws: sjsdr.WebSocket,
@ -24,49 +32,84 @@ class WebSocketImpl[T: Encoder: Decoder](
) {
val printer = Printer.noSpaces
lazy val source: Task[Observable[T]] =
val source: Task[Observable[T]] =
Task.deferAction(implicit s =>
for {
c <- Task(CompositeCancelable())
obs <- Task(
Observable
.create[T](overflowStrategy) { sub =>
// val c = SingleAssignCancelable()
ws.onmessage = (e: MessageEvent) =>
e.data match {
case s: String =>
decode[T](s).map(sub.onNext).left.foreach(println)
case other => println(other)
decode[T](s)
.map { res =>
if (sub.onNext(res) == Ack.Stop) c.cancel()
res
}
.left
.foreach(err =>
sub.onError(
new Exception(s"Failed to decode $s. Error was $err")
)
)
case other =>
sub.onError(new Exception(s"Received wrong type: $other"))
}
ws.onerror = (e: Event) =>
ws.onclose = a => {
println(
s"Websocket closing - ${a.code} ${a.wasClean} ${a.reason}"
)
sub.onComplete()
}
ws.onerror = (e: Event) => {
println("Error in websocket")
sub.onError(new Exception(s"Error in WebSocket: $e"))
Cancelable(() => ws.close())
}
c += Cancelable { () =>
println("Closing websocket")
ws.close()
}
}
.publish
.refCount
)
_ <- Task(obs.subscribe(Observer.empty))
// empty subscription because otherwise ouwtatch kills the observable when
// the dom containing this observable is unmounted
_ <- Task(c += obs.subscribe(Observer.empty))
} yield obs
)
lazy val sink: Task[Observer[T]] =
Task(
new Observer[T] {
override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson)
ws.send(msg)
Future.successful(Ack.Continue)
}
override def onError(ex: Throwable): Unit = println(ex)
override def onComplete(): Unit = ()
}
val sink: Task[Subscriber[T]] =
Task.deferAction(implicit s =>
Task(
BufferedSubscriber(
new Subscriber[T] {
override implicit def scheduler: Scheduler = s
override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson)
if (ws.readyState == 2 || ws.readyState == 3)
// Future.failed(new Exception("Websocket closed"))
Future.successful(Ack.Stop)
else {
ws.send(msg)
Future.successful(Ack.Continue)
}
}
override def onError(ex: Throwable): Unit = println(ex)
override def onComplete(): Unit = ()
},
OverflowStrategy.BackPressure(50)
)
)
)
}
object WebSocket {
sealed trait Error
final case object Error extends Error
// val w = new sjsdr.Worker("/worker.js").asInstanceOf[Worker]
// type MonixWebWorker[T] = MonixProSubject[T, T]
def apply[T <: Product: Encoder: Decoder](
path: String,
@ -74,6 +117,11 @@ object WebSocket {
OverflowStrategy.DropOld(50)
): Task[WebSocket[T]] = for {
websocket <- Task(new sjsdr.WebSocket(path))
p <- Task(CancelablePromise[Unit]())
// wait for websocket to open
_ <- Task(websocket.onopen = _ => p.success(()))
_ <- Task.deferFuture(p.future).timeout(10.seconds)
_ <- Task(websocket.onopen = _ => ())
impl = new WebSocketImpl[T](websocket, overflowStrategy)
source <- impl.source
sink <- impl.sink

111
src/main/scala/outwatchapp/util/reactive/WebWorker.scala

@ -11,99 +11,98 @@ import io.circe.syntax._
import monix.bio.Task
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import outwatchapp.util.reactive.MonixProSubject
import org.scalajs.dom.raw.Event
import org.scalajs.dom.raw.MessageEvent
import org.scalajs.dom.{raw => sjsdr}
import outwatchapp.util.reactive.MonixProSubject
import monix.execution.cancelables.CompositeCancelable
import monix.eval.Coeval
import monix.reactive.observers.Subscriber
import monix.execution.Scheduler
import monix.reactive.observers.BufferedSubscriber
import outwatchapp.util.reactive.Exceptions.DecodeException
import outwatchapp.util.reactive.Exceptions.WrongTypeException
import outwatchapp.util.reactive.Exceptions.TerminatedException
class WebWorkerImpl[T <: Product: Encoder: Decoder](
worker: sjsdr.Worker,
overflowStrategy: OverflowStrategy.Synchronous[T]
) {
// private def parseFn(data: T) = {
// data match {
// case _: AnyRef => parseRef(data)
// case s: String =>
// case other =>
// println(other)
// Left(WebWorker.Error)
// }
// }
// private def parseRef(data: T): Either[WebWorker.Error, T] = {
// data match {
// case s: String => decode[T](s).leftMap(_ => WebWorker.Error)
// case a: Int =>
// println(a)
// Left(WebWorker.Error)
// case other =>
// println(other)
// Left(WebWorker.Error)
// }
// }
// private def parsePrimitive(data: T): Either[WebWorker.Error, T] = {
// data match {
// case s: String => decode[T](s).leftMap(_ => WebWorker.Error)
// case a: Int =>
// println(a)
// Left(WebWorker.Error)
// case other =>
// println(other)
// Left(WebWorker.Error)
// }
// }
// println(s"Got data $a")
// .map(sub.onNext).left.foreach(println)
val printer = Printer.noSpaces
lazy val source: Task[Observable[T]] =
val source: Task[Observable[T]] =
Task.deferAction(implicit s =>
for {
c <- Task(CompositeCancelable())
obs <- Task(
Observable
.create[T](overflowStrategy) { sub =>
// val c = SingleAssignCancelable()
worker.onmessage = (e: MessageEvent) =>
e.data match {
case s: String =>
decode[T](s).map(sub.onNext).left.foreach(println)
case other => println(other)
decode[T](s)
.map { res =>
if (sub.onNext(res) == Ack.Stop) c.cancel()
res
}
.left
.foreach(err =>
sub.onError(
DecodeException(
s"Failed to decode $s. Error was $err"
)
)
)
case other =>
sub.onError(
WrongTypeException(s"Received wrong type: $other")
)
}
worker.onerror = (e: Event) =>
sub.onError(new Exception(s"Error in WebSocket: $e"))
Cancelable(() => worker.terminate())
sub.onError(
TerminatedException(s"Worker terminated with error: $e")
)
c += Cancelable(() => worker.terminate())
}
.doOnSubscriptionCancelF(Coeval(println("Worker cancelled")))
.publish
.refCount
)
_ <- Task(obs.subscribe(Observer.empty))
_ <- Task(c += obs.subscribe(Observer.empty))
} yield obs
)
lazy val sink: Task[Observer[T]] =
Task(
new Observer[T] {
override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson)
worker.postMessage(msg)
Future.successful(Ack.Continue)
}
override def onError(ex: Throwable): Unit = println(ex)
override def onComplete(): Unit = ()
}
val sink: Task[Observer[T]] =
Task.deferAction(implicit s =>
Task(
BufferedSubscriber(
new Subscriber[T] {
override implicit def scheduler: Scheduler = s
override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson)
worker.postMessage(msg)
Ack.Continue
}
override def onError(ex: Throwable): Unit = s.reportFailure(ex)
override def onComplete(): Unit = ()
},
OverflowStrategy.Default
)
)
)
}
object WebWorker {
sealed trait Error
final case object Error extends Error
// val w = new sjsdr.Worker("/worker.js").asInstanceOf[Worker]
// type MonixWebWorker[T] = MonixProSubject[T, T]
def apply[T <: Product: Encoder: Decoder](
path: String,

1
src/main/scala/outwatchapp/util/reactive/package.scala

@ -8,4 +8,5 @@ package object reactive {
type MonixSubject[A] = MonixProSubject[A, A]
type WebWorker[A] = MonixSubject[A]
type WebSocket[A] = MonixSubject[A]
type DedicatedWorker[A] = MonixSubject[A]
}

23690
src/main/scala/outwatchapp/util/reactive/store/test.css
File diff suppressed because it is too large
View File

67
yarn.lock

@ -2,6 +2,11 @@
# yarn lockfile v1
"@sweetalert2/themes@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@sweetalert2/themes/-/themes-4.0.1.tgz#ba07a40743fd439663341480c20360116c2e1f27"
integrity sha512-dE+SXd0ym2RUkDQT/rVyBIwe6QIKl1cfBF3133XdLeXVKUR+XEtkr1uVBK821uX5DRQfN9doJmVCd2jFdG/qXw==
"@types/chart.js@2.9.11":
version "2.9.11"
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.11.tgz#2b73fe59e78dfe31c2f8d6c6d0c169e98e65c16b"
@ -9,6 +14,13 @@
dependencies:
moment "^2.10.2"
"@types/datatables.net@1.10.19":
version "1.10.19"
resolved "https://registry.yarnpkg.com/@types/datatables.net/-/datatables.net-1.10.19.tgz#17c5f94433f761086131c6c8dc055a0e1099d1f9"
integrity sha512-WuzgytEmsIpVYZbkce+EvK1UqBI7/cwcC/WgYeAtXdq2zi+yWzJwMT5Yb6irAiOi52DBjeAEeRt3bYzFYvHWCQ==
dependencies:
"@types/jquery" "*"
"@types/glob@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@ -17,6 +29,13 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/jquery@*", "@types/jquery@3.5.5":
version "3.5.5"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903"
integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==
dependencies:
"@types/sizzle" "*"
"@types/json-schema@^7.0.6":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
@ -32,6 +51,16 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b"
integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==
"@types/paralleljs@0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@types/paralleljs/-/paralleljs-0.0.20.tgz#5013416ccb05d2fec5e2bcf5331d37bd203abe44"
integrity sha512-+1y+7aNWEvsXfdubI9y5aUFYU7RiUgVqAo+Px9siGLPUq4RZodvrP2EdoCV+oPQdcgKuQnW+CuikWcUSZuAkhA==
"@types/sizzle@*":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
"@webassemblyjs/ast@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@ -1010,6 +1039,21 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
datatables.net-bs4@1.10.23:
version "1.10.23"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.10.23.tgz#bdd5d0dcb1bd68a7afe649a4424ba20647523b52"
integrity sha512-ChUB8t5t5uzPnJYTPXx2DOvnlm2shz8OadXrKoFavOadB308OuwHVxSldYq9+KGedCeiVxEjNqcaV4nFSXkRsw==
dependencies:
datatables.net "1.10.23"
jquery ">=1.7"
datatables.net@1.10.23:
version "1.10.23"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.23.tgz#59f7d7b12845183b1b379530d1385077e113ec01"
integrity sha512-we3tlNkzpxvgkKKlTxTMXPCt35untVXNg8zUYWpQyC1U5vJc+lT0+Zdc1ztK8d3lh5CfdnuFde2p8n3XwaGl3Q==
dependencies:
jquery ">=1.7"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -2190,10 +2234,10 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
jquery@3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
jquery@3.5.1, jquery@>=1.7:
version "3.5.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
json-parse-better-errors@^1.0.2:
version "1.0.2"
@ -2836,6 +2880,11 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
paralleljs@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/paralleljs/-/paralleljs-1.1.0.tgz#d0cd5540fe4dd8e80fa94d3cb6a530510dbe6e70"
integrity sha512-5sYLAEzM+qMDYzQeVWyNdqINU4h+/Fhr0CrZs3+HCYPIhOWuA2BCzn74js2qzX+LKFwRQR6sSJBZhgT+WWhbfw==
parse-asn1@^5.0.0, parse-asn1@^5.1.5:
version "5.1.6"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
@ -3176,6 +3225,11 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
reconnecting-websocket@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@ -3751,6 +3805,11 @@ supports-color@^6.1.0:
dependencies:
has-flag "^3.0.0"
sweetalert2@10.12.6:
version "10.12.6"
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-10.12.6.tgz#249b95fecb28986a1aff82710a460ecb4717e2a9"
integrity sha512-kkPRpNqP0wF9tAVu1Ygp4pQx4pXY/pgseyW3dOXXRu0S6s7HKfHEmpBUIZrBylRfeJFPOsSEk6sALSwKzNZ9RQ==
tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"

Loading…
Cancel
Save