You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
5.3 KiB
165 lines
5.3 KiB
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)
|
|
}
|