many changes

This commit is contained in:
Rohan Sircar 2021-01-30 13:50:43 +05:30
parent d455175044
commit 08b320b051
25 changed files with 25211 additions and 155 deletions

View File

@ -6,6 +6,9 @@
<title>Outwatch App</title> <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"> <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> </head>

View File

@ -35,18 +35,28 @@ libraryDependencies ++= Seq(
"com.beachape" %%% "enumeratum-circe" % "1.6.1", "com.beachape" %%% "enumeratum-circe" % "1.6.1",
"com.github.valskalla" %%% "odin-core" % "0.7.0+95-ab4381ae+20201227-1831-SNAPSHOT", "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" % "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" // "com.clovellytech" %%% "outwatch-router" % "0.0.9+7-5be0b1a2+20201227-2019-SNAPSHOT"
) )
Compile / npmDependencies ++= Seq( Compile / npmDependencies ++= Seq(
"jquery" -> "3.3.1", "jquery" -> "3.5.1",
"@types/jquery" -> "3.5.5",
"blk-design-system" -> "1.0.2", "blk-design-system" -> "1.0.2",
"bootstrap" -> "4.5.3", "bootstrap" -> "4.5.3",
"@types/chart.js" -> "2.9.11", "@types/chart.js" -> "2.9.11",
"chart.js" -> "2.9.3", "chart.js" -> "2.9.3",
"snabbdom" -> "git://github.com/outwatch/snabbdom.git#semver:0.7.5", "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( Compile / npmDevDependencies ++= Seq(
@ -57,8 +67,14 @@ Compile / npmDevDependencies ++= Seq(
"url-loader" -> "1.1.2" "url-loader" -> "1.1.2"
) )
stIgnore ++= List("jquery", "blk-design-system", "bootstrap") stIgnore ++= List(
stIgnore ++= List("snabbdom") "datatables.net-bs4",
"datatables.net-dt",
"blk-design-system",
"bootstrap",
"snabbdom",
"@sweetalert2/themes"
)
stStdlib := List("es6") stStdlib := List("es6")
stUseScalaJsDom := false stUseScalaJsDom := false

View File

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

View File

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

View File

@ -8,28 +8,47 @@ import io.odin.consoleLogger
import monix.bio.Task import monix.bio.Task
import monix.eval.Coeval import monix.eval.Coeval
import monix.reactive.Observable import monix.reactive.Observable
import monix.{eval => me}
import org.scalajs.dom.raw.Element import org.scalajs.dom.raw.Element
import outwatch._ import outwatch._
import outwatch.dsl._ import outwatch.dsl._
import outwatch.router._ import outwatch.router._
import outwatchapp.components.ChartjsDemo
import outwatchapp.components.CounterDemo import outwatchapp.components.CounterDemo
import outwatchapp.components.DatatablesDemo
import outwatchapp.components.FusejsDemo
import outwatchapp.components.RequestDemo import outwatchapp.components.RequestDemo
import outwatchapp.components.todo.ChartjsDemo
import outwatchapp.components.todo.FusejsDemo
import outwatchapp.pages.HomePage import outwatchapp.pages.HomePage
import outwatchapp.ui.components.todo.TodoListStore import outwatchapp.ui.components.todo.TodoListStore
import outwatchapp.util.IOUtils
import outwatchapp.util.reactive.WebSocket
import outwatchapp.util.reactive.WebWorker import outwatchapp.util.reactive.WebWorker
import outwatchapp.util.reactive.WebsocketData
import outwatchapp.util.reactive.WorkerData 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 class MainApp(el: Element)(implicit
backend: AppTypes.Backend, backend: AppTypes.Backend,
store: RouterStore[Page] store: RouterStore[Page]
) { ) {
import MainApp._
def run: Task[Unit] = for { def run: Task[Unit] = for {
resolver <- resolver resolver <- resolver
_ <- OutWatch.renderInto[Task](el, Router(resolver)) _ <- OutWatch.renderInto[Task](el, Router(resolver))
} yield () } 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]] = def resolver: Task[PartialFunction[Page, VDomModifier]] =
Task.deferAction(implicit s => Task.deferAction(implicit s =>
for { for {
@ -38,35 +57,131 @@ class MainApp(el: Element)(implicit
todoStore <- TodoListStore(consoleLogger[Task]()) todoStore <- TodoListStore(consoleLogger[Task]())
requestDemo <- RequestDemo(todoStore) requestDemo <- RequestDemo(todoStore)
demoWorker <- WebWorker[WorkerData]("/worker.js") 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 { } yield {
case Page.Home => wire[HomePage].render case Page.Home => wire[HomePage].render
case Page.SomePage => case Page.SomePage =>
div( div(
div(cls := "title", "SomePage"), div(cls := "title", "SomePage"),
// RequestDemo(todoStore), // RequestDemo(todoStore),
Observable // Observable
.interval(1.second) // .interval(1.second)
.doOnNextF(i => Coeval(println(s"Producer emitted $i"))) // .doOnNextF(i => Coeval(println(s"Producer emitted $i")))
.doOnNextF(i => // .doOnNextF(i =>
Coeval(demoWorker.onNext(WorkerData(i))) >> Coeval.unit // Coeval(demoWorker.onNext(WorkerData(i))) >> Coeval.unit
) // )
.map(_ => div()), // .take(2)
// .map(_ => div()),
demoWorker.map(_.toString).map(v => p(cls := "text-white", v)), demoWorker.map(_.toString).map(v => p(cls := "text-white", v)),
requestDemo, requestDemo,
div(cls := "slider") div(cls := "slider")
) )
case Page.UserHome(id) => case Page.UserHome(id) =>
div( div(
cls := "text-white", // cls := "text-white",
div(cls := "title", "UserHome"), div(cls := "title", "UserHome"),
s"User id: $id", 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 => case Page.NotFound =>
Task(
div( div(
div(cls := "title", "NotFound"), cls := "page-header error-page header-filter",
p(cls := "text-white", "notfound") 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)
}

View File

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

View File

@ -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]
}
}

View File

@ -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")
)
)
)
}

View File

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

View File

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

View File

@ -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)
}

View File

@ -1,11 +1,19 @@
package outwatchapp package outwatchapp
import scala.util.Try
import colibri.LiftSink import colibri.LiftSink
import colibri.LiftSource import colibri.LiftSource
import colibri.ext.monix._ import colibri.ext.monix._
import monix.bio.Task
import monix.execution.Scheduler import monix.execution.Scheduler
import monix.reactive.Observable import monix.reactive.Observable
import monix.reactive.Observer 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 { package object implicits {
// implicit def sinkForMyMonixProSub[A, M] = // 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) @inline def liftSink[G[_]: LiftSink]: G[A] = LiftSink[G].lift(sink)
} }
implicit class Ops2[A](val source: Observable[A])(implicit s: Scheduler) { implicit class Ops2[A](val source: Observable[A]) extends AnyVal {
@inline def liftSource[G[_]: LiftSource]: G[A] = LiftSource[G].lift(source) @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
} }
} }

View File

@ -1,15 +1,28 @@
package outwatchapp.pages package outwatchapp.pages
import cats.syntax.eq._
import com.softwaremill.tagging._ import com.softwaremill.tagging._
import monix.bio.Task
import outwatch._ import outwatch._
import outwatch.dsl._ import outwatch.dsl._
import outwatchapp.components.ChartjsDemo
import outwatchapp.components.CounterDemo import outwatchapp.components.CounterDemo
import outwatchapp.components.todo.ChartjsDemo import outwatchapp.components.SweetAlertDemo
class HomePage( class HomePage(
counterDemo: VNode @@ CounterDemo, counterDemo: VNode @@ CounterDemo,
chartDemo: VNode @@ ChartjsDemo chartDemo: VNode @@ ChartjsDemo
) { ) {
def render = div( 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(
div(cls := "title", "Home"), div(cls := "title", "Home"),
div( div(
cls := "card", cls := "card",
@ -17,6 +30,14 @@ class HomePage(
cls := "card-body", cls := "card-body",
counterDemo, counterDemo,
chartDemo, chartDemo,
div(
cls := "text-center",
button(
cls := "btn btn-primary",
onClick.preventDefault.doAsync(loginDemo),
"Login"
)
),
p( p(
cls := "text-white", cls := "text-white",
div( div(
@ -38,4 +59,6 @@ class HomePage(
) )
) )
) )
)
)
} }

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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")
// }
// }
// }

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,6 +1,7 @@
package outwatchapp.util.reactive package outwatchapp.util.reactive
import org.scalajs.dom.{raw => sjsdr} import scala.concurrent.Future
import io.circe.Decoder import io.circe.Decoder
import io.circe.Encoder import io.circe.Encoder
import io.circe.Printer import io.circe.Printer
@ -10,13 +11,20 @@ import io.circe.syntax._
import monix.bio.Task import monix.bio.Task
import monix.execution.Ack import monix.execution.Ack
import monix.execution.Cancelable import monix.execution.Cancelable
import monix.execution.CancelablePromise
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable import monix.reactive.Observable
import monix.reactive.Observer import monix.reactive.Observer
import monix.reactive.OverflowStrategy import monix.reactive.OverflowStrategy
import outwatchapp.util.reactive.MonixProSubject
import org.scalajs.dom.raw.Event import org.scalajs.dom.raw.Event
import org.scalajs.dom.raw.MessageEvent 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]( class WebSocketImpl[T: Encoder: Decoder](
ws: sjsdr.WebSocket, ws: sjsdr.WebSocket,
@ -24,49 +32,84 @@ class WebSocketImpl[T: Encoder: Decoder](
) { ) {
val printer = Printer.noSpaces val printer = Printer.noSpaces
lazy val source: Task[Observable[T]] = val source: Task[Observable[T]] =
Task.deferAction(implicit s => Task.deferAction(implicit s =>
for { for {
c <- Task(CompositeCancelable())
obs <- Task( obs <- Task(
Observable Observable
.create[T](overflowStrategy) { sub => .create[T](overflowStrategy) { sub =>
// val c = SingleAssignCancelable()
ws.onmessage = (e: MessageEvent) => ws.onmessage = (e: MessageEvent) =>
e.data match { e.data match {
case s: String => case s: String =>
decode[T](s).map(sub.onNext).left.foreach(println) decode[T](s)
case other => println(other) .map { res =>
if (sub.onNext(res) == Ack.Stop) c.cancel()
res
} }
ws.onerror = (e: Event) => .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.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")) sub.onError(new Exception(s"Error in WebSocket: $e"))
Cancelable(() => ws.close()) }
c += Cancelable { () =>
println("Closing websocket")
ws.close()
}
} }
.publish .publish
.refCount .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 } yield obs
) )
lazy val sink: Task[Observer[T]] = val sink: Task[Subscriber[T]] =
Task.deferAction(implicit s =>
Task( Task(
new Observer[T] { BufferedSubscriber(
new Subscriber[T] {
override implicit def scheduler: Scheduler = s
override def onNext(elem: T): Future[Ack] = { override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson) 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) ws.send(msg)
Future.successful(Ack.Continue) Future.successful(Ack.Continue)
} }
}
override def onError(ex: Throwable): Unit = println(ex) override def onError(ex: Throwable): Unit = println(ex)
override def onComplete(): Unit = () override def onComplete(): Unit = ()
} },
OverflowStrategy.BackPressure(50)
)
)
) )
} }
object WebSocket { object WebSocket {
sealed trait Error sealed trait Error
final case object Error extends 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]( def apply[T <: Product: Encoder: Decoder](
path: String, path: String,
@ -74,6 +117,11 @@ object WebSocket {
OverflowStrategy.DropOld(50) OverflowStrategy.DropOld(50)
): Task[WebSocket[T]] = for { ): Task[WebSocket[T]] = for {
websocket <- Task(new sjsdr.WebSocket(path)) 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) impl = new WebSocketImpl[T](websocket, overflowStrategy)
source <- impl.source source <- impl.source
sink <- impl.sink sink <- impl.sink

View File

@ -11,99 +11,98 @@ import io.circe.syntax._
import monix.bio.Task import monix.bio.Task
import monix.execution.Ack import monix.execution.Ack
import monix.execution.Cancelable import monix.execution.Cancelable
import monix.execution.cancelables.SingleAssignCancelable
import monix.reactive.Observable import monix.reactive.Observable
import monix.reactive.Observer import monix.reactive.Observer
import monix.reactive.OverflowStrategy import monix.reactive.OverflowStrategy
import outwatchapp.util.reactive.MonixProSubject
import org.scalajs.dom.raw.Event import org.scalajs.dom.raw.Event
import org.scalajs.dom.raw.MessageEvent import org.scalajs.dom.raw.MessageEvent
import org.scalajs.dom.{raw => sjsdr} 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]( class WebWorkerImpl[T <: Product: Encoder: Decoder](
worker: sjsdr.Worker, worker: sjsdr.Worker,
overflowStrategy: OverflowStrategy.Synchronous[T] 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 val printer = Printer.noSpaces
lazy val source: Task[Observable[T]] = val source: Task[Observable[T]] =
Task.deferAction(implicit s => Task.deferAction(implicit s =>
for { for {
c <- Task(CompositeCancelable())
obs <- Task( obs <- Task(
Observable Observable
.create[T](overflowStrategy) { sub => .create[T](overflowStrategy) { sub =>
// val c = SingleAssignCancelable()
worker.onmessage = (e: MessageEvent) => worker.onmessage = (e: MessageEvent) =>
e.data match { e.data match {
case s: String => case s: String =>
decode[T](s).map(sub.onNext).left.foreach(println) decode[T](s)
case other => println(other) .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) => worker.onerror = (e: Event) =>
sub.onError(new Exception(s"Error in WebSocket: $e")) sub.onError(
Cancelable(() => worker.terminate()) TerminatedException(s"Worker terminated with error: $e")
)
c += Cancelable(() => worker.terminate())
} }
.doOnSubscriptionCancelF(Coeval(println("Worker cancelled")))
.publish .publish
.refCount .refCount
) )
_ <- Task(obs.subscribe(Observer.empty)) _ <- Task(c += obs.subscribe(Observer.empty))
} yield obs } yield obs
) )
lazy val sink: Task[Observer[T]] = val sink: Task[Observer[T]] =
Task.deferAction(implicit s =>
Task( Task(
new Observer[T] { BufferedSubscriber(
new Subscriber[T] {
override implicit def scheduler: Scheduler = s
override def onNext(elem: T): Future[Ack] = { override def onNext(elem: T): Future[Ack] = {
val msg = printer.print(elem.asJson) val msg = printer.print(elem.asJson)
worker.postMessage(msg) worker.postMessage(msg)
Future.successful(Ack.Continue) Ack.Continue
} }
override def onError(ex: Throwable): Unit = println(ex) override def onError(ex: Throwable): Unit = s.reportFailure(ex)
override def onComplete(): Unit = () override def onComplete(): Unit = ()
} },
OverflowStrategy.Default
)
)
) )
} }
object WebWorker { object WebWorker {
sealed trait Error sealed trait Error
final case object Error extends 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]( def apply[T <: Product: Encoder: Decoder](
path: String, path: String,

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,11 @@
# yarn lockfile v1 # 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": "@types/chart.js@2.9.11":
version "2.9.11" version "2.9.11"
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.11.tgz#2b73fe59e78dfe31c2f8d6c6d0c169e98e65c16b" resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.11.tgz#2b73fe59e78dfe31c2f8d6c6d0c169e98e65c16b"
@ -9,6 +14,13 @@
dependencies: dependencies:
moment "^2.10.2" 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": "@types/glob@^7.1.1":
version "7.1.3" version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@ -17,6 +29,13 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@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": "@types/json-schema@^7.0.6":
version "7.0.6" version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" 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" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b"
integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw== 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": "@webassemblyjs/ast@1.9.0":
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" 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" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= 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: debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 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" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
jquery@3.3.1: jquery@3.5.1, jquery@>=1.7:
version "3.3.1" version "3.5.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
json-parse-better-errors@^1.0.2: json-parse-better-errors@^1.0.2:
version "1.0.2" version "1.0.2"
@ -2836,6 +2880,11 @@ parallel-transform@^1.1.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.1.5" 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: parse-asn1@^5.0.0, parse-asn1@^5.1.5:
version "5.1.6" version "5.1.6"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
@ -3176,6 +3225,11 @@ readdirp@~3.5.0:
dependencies: dependencies:
picomatch "^2.2.1" 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: regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@ -3751,6 +3805,11 @@ supports-color@^6.1.0:
dependencies: dependencies:
has-flag "^3.0.0" 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: tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"