by

Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
  • Sep 19 19:58
    keynmol edited #66
  • Sep 19 19:58
    keynmol edited #66
  • Sep 14 01:51

    raquo on static-site

    Fix circle ci yaml, maybe (compare)

  • Sep 14 01:49
    raquo synchronize #66
  • Sep 14 01:45
    raquo edited #66
  • Sep 14 01:44
    raquo edited #66
  • Sep 14 01:44
    raquo edited #66
  • Sep 14 01:42
    raquo synchronize #66
  • Sep 14 01:42

    raquo on static-site

    Fix circleci yaml (compare)

  • Sep 14 01:38
    raquo edited #66
  • Sep 14 01:38
    raquo edited #66
  • Sep 14 01:37
    raquo ready_for_review #66
  • Sep 14 01:37
    raquo converted_to_draft #66
  • Sep 14 01:37

    raquo on static-site

    Use the same version of Scala e… Fix website publish config Rename website sbt projects, up… and 1 more (compare)

  • Sep 14 01:36
    raquo commented #66
  • Sep 14 01:36
    raquo edited #66
  • Sep 14 01:34
    raquo synchronize #66
  • Sep 14 01:30

    raquo on gh-pages

    Deploy website Deploy website … (compare)

  • Sep 14 01:03
    raquo edited #66
  • Sep 14 00:58

    raquo on gh-pages

    Deploy website Deploy website … (compare)

Victor Borja
@vic
Also, any pointers on why the default strategy is a switchStream (and hence we stop listenning to previously produced streams) would be awesome.
Victor Borja
@vic
-- I'm guessing it has something to do with the fact that EventStreams do not finish? and having a mergeStrategy would create lotss of subscriptions ? dunno.
Nikita Gazarov
@raquo
The idea of SwitchStreamStrategy is that previous input values become stale, and so their results are no longer desired. It's useful when making network requests, e.g. if you have a component that needs to fetch and display a user name, and it accepts an EventStream[UserID] as input, so it has something like userIdStream.flatMap(fetchUser).map(user => div(user.name)). SwitchStreamStrategy here means you don't need to think about the timing of network requests, "what if request A completes after request B, resulting in incorrect / stale output at the end). SwitchStreamStrategy is not the only way to achieve this logic, it's just the simplest one that's already implemented.
Nikita Gazarov
@raquo
I'm looking into getting a merge stream strategy
Nikita Gazarov
@raquo
@ngbinh I think raquo/Airstream#38 will do what you want, you can check it out locally with publishLocal for now
Iurii Malchenko
@yurique

Nice! :)
I might have wanted something like this at some point, but no longer remember the use case.

Here's another interesting (and somewhat related) thing that is quite tricky to implement right now, but might be useful sometimes (I have something like this in a thing that looks like FSM but with strrams): I don't know the name for it, so I'll call it "recursive stream".

Like this:

type RecStream[T] = EventStream[(T,  RecStream[T])]

val initial: RecStream[T] = ???
val result: EventStream[T] = EventStream.rec[T](initial)

What do you guys think?

Nikita Gazarov
@raquo
So.... "Perform the returned async action once the previous one completes, over and over, and emit the values returned by all these actions". Sounds interesting, what's your concrete use case for that?
I mean I understand the async-state-machine concept (whatever it's called, indeed), just curious for an actual example
Victor Borja
@vic
@raquo that new ConcurrentStreamStrategy looks great, will try it tomorrow. thanks a lot !
Binh Nguyen
@ngbinh
@raquo awesome, very happy Laminar user now.
Nikita Gazarov
@raquo
:tada:
Nikita Gazarov
@raquo
Published Airstream v0.10.2 with ConcurrentStreamStrategy
Iurii Malchenko
@yurique

just curious for an actual example

I have this thing:

object AirFSM {
  def bind[State]($initial: EventStream[State])(
      control: PartialFunction[(Option[State], State), EventStream[State]]
    ): Binder[ReactiveElement.Base] = ???
}

There's State, which might be something like this:

sealed trait State extends Product with Serializable
object State {
  case object Initial                                                    extends State
  final case class Step1()                                               extends State
  final case class Step2(inputFromStep1: String)                         extends State
  final case class Step3(inputFromStep1: String, inputFromStep2: String) extends State
}

And the bind function, which accepts:

  • an initial stream of state change requests;
  • and a control (partial) function which will be receiving a tuple of (previousState, nextState)
    (state transition)

The control function:

  • executes whatever effects needed for the state transition at hand;
  • returns a new stream of state change requests.
div(
  child <-- currentStateView.signal,
  AirFSM.bind[State](initialStateChangeRequests) {
    case (_, State.Initial)                         => enterInitialState
    case (Some(State.Initial), state: State.Step1)  => enterStep1(state)
    case (Some(_: State.Step1), state: State.Step2) => enterStep2(state)
    case (Some(_: State.Step2), state: State.Step3) => enterStep3(state)
    // case (prev, next) => println(s"invalid state transition: $prev -> $next")
  }
)

(full example: https://gist.github.com/yurique/68ec570c0b6b3cfefc316796b2863748)
(AirFSM: https://gist.github.com/yurique/e4e81c28fd5c35549b63ce7a7e232cc5)

Iurii Malchenko
@yurique

So, having a EventStream.rec from earlier would make implementing AirFSM.bind easier (I think) :)

As well as a transitions function on a stream, but that’s easy to 3rd-party:

  implicit class EventStreamExt[A](val s: EventStream[A]) extends AnyVal {

    def transitions: EventStream[(Option[A], A)] = {
      EventStream
        .merge(
          EventStream.fromValue(Option.empty[A], emitOnce = true),
          s.map(a => Some(a))
        ).combineWith(
          s.drop(1)
        )
    }

    def drop(count: Int): EventStream[A] = {
      var seen = 0
      s.filter(_ => seen >= count).map { event =>
        seen = seen + 1
        event
      }
    }

    def take(count: Int): EventStream[A] = {
      var seen = 0
      s.filter(_ => seen < count).map { event =>
        seen = seen + 1
        event
      }
    }
  }
3 replies
(actually I think the initial param can go)
Iurii Malchenko
@yurique
(yeah, updated the gists)
(the custom implementation of transitions, as well as take and drop, is probably not the most efficient one)
Anton Sviridov
@keynmol
@raquo do you publish laminar from your local, not from CI?
1 reply
Victor Borja
@vic
I've been using Laminar at work for about a year, with my amazing friend @davoclavo, and I've been loving it so much. I'd love if more people could know about it, Scala.js is awesome for typed front-end apps, and Laminar is just very comfortable and natural. Now I'm thinking of doing some twitch streams and live-demo building a simple app, just to get people curious about it. Currently thinking about doing it in Spanish, since I'm not fluent at all speaking english (perhaps I'd like to do that later). So, if you know of someone in LatAm or spanish-speaking people who would benefit or simply would be curious about learning Laminar, tell them to contact me: https://twitter.com/oeiuwq/status/1304679399921512448
6 replies
sparlampe
@sparlampe
hi all, how do I use html entities with laminar? I need to have a non-breaking space in a line of text div("some&nbsp;Text") but the entity is getting escaped. What is the right approach here?
Iurii Malchenko
@yurique
Haminar has nbsp (since a while ago)
3 replies
so you’d do something like div(“some”, nbsp, “text”) or div(s”some${nbsp}text)
Anton Sviridov
@keynmol
@yurique off the top of your head do you know where the escaping happens?
Iurii Malchenko
@yurique
@keynmol I’m not sure I got what you mean :)
Anton Sviridov
@keynmol
nevermind, I realised that Laminar/scalajs are not doing the escaping, it's just JS/HTML works :)
Anton Sviridov
@keynmol

I'm on the hunt for a good websockets component for laminar, and in the meantime I'm building out my own: https://github.com/keynmol/laminar-components/, could appreciate some feedback on the implementation

Don't be gentle - I'm a backend engineer and Laminar is pretty much the only thing that made me interesting in doing any frontend :D so I don't know much.

Iurii Malchenko
@yurique
@keynmol I’ve had this a while ago: https://gist.github.com/yurique/69f9c245959bf1ae20d1cbc3df3f1cd9
(it needs some love, but I haven’t worked with websockets in a while)
looks pretty similar (and if I were to do this today I’d use the binder with start stop, too :) )
Anton Sviridov
@keynmol

I’d use the binder with start stop, too

Because I stole it from you, sorry :)

But yeah, it looks almost identical - I need to read deeper into the whole subscription business but I think your binder handles that?

Your secret gists are a goldmine, probably :D

it's good to see that your modelling is similar - I want to improve mine around error handling and add some tests,.

Also want to keep it flexible as I do have some projects that are not tied to circe (ujson) or not using any non-primitive protocol at all

Nikita Gazarov
@raquo
My own web socket client exposes an Observer[ClientMessage] and an EventStream[ServerMessage] so it doesn't need binding logic
I use sealed traits with upickle codecs for type safe comms
ngochai94
@ngochai94

hi guys, is there a way to combine more than 2 Signal?

val mySignals: Seq[Signal[Int]] = ???
val combinedSignal: Signal[Seq[Int]] = ???

I'm thinking about folding the signals and combine 1 by 1, but is there a better way?

Iurii Malchenko
@yurique

I haven’t approached this in a while, but I found this in my code:

  implicit class SignalObjExt(val s: Signal.type) extends AnyVal {

    def seq[A](signals: Seq[Signal[A]]): Signal[Seq[A]] =
      if (signals.isEmpty) {
        Val(Seq.empty)
      } else {
        signals
          .drop(1).foldLeft[Signal[Seq[A]]](signals.head.map(Seq(_)))(
            (acc, next) => acc.combineWith(next).map2(_ :+ _)
          )
      }

  }

(though I don’t think I gave it too much thought back then)

Iurii Malchenko
@yurique
(it’s definitely over-complicated :) )
this is simpler, at least:
    def seq[A](signals: Seq[Signal[A]]): Signal[Seq[A]] =
      signals.foldLeft[Signal[Seq[A]]](Val(Seq.empty))(
        (acc, next) => acc.combineWith(next).map2(_ :+ _)
      )
Iurii Malchenko
@yurique

this should a bit more efficient:


  implicit class SignalExt[A](val s: Signal[A]) extends AnyVal {

    def combineWithMap[B, C](otherSignal: Signal[B])(project: (A, B) => C): Signal[C] = 
      new CombineSignal2[A, B, C](
        parent1 = s,
        parent2 = otherSignal,
        combinator = CombineObservable.guardedCombinator(project)
      )    

  }


  implicit class SignalObjExt(val s: Signal.type) extends AnyVal {

    def seq[A](signals: Seq[Signal[A]]): Signal[Seq[A]] =
      signals.foldLeft[Signal[Seq[A]]](Val(Seq.empty))(
        (acc, next) => acc.combineWithMap(next)(_ :+ _)
      )

  }

@raquo do you think some of this deserves to live in Airstream core? (combineWithMap, maybe named properly, seems generic enough and could optimize the s1.combineWith(s2).map2(...) use case a bit)

and seq — I suppose it’s possible to make it more efficient by implementing it at a lower level with it’s own SeqSignal[A], but I have no idea how, at the moment :)
ngochai94
@ngochai94
Thank you @yurique ! I did something similar to your 2nd code snippet and it works well for me.
Nikita Gazarov
@raquo

@yurique I don't think I want to expose the combine combinator because it breaks the "shared execution" expectation of Airstream. When you have stream.map(project), you know that project will be called at most once per incoming event. But that's not the case for the combine combinator, it will be called on every upstream event, and there could be more of those than resulting downstream events if one of upstream observables synchronously depends on the other (e.g. diamond case)

As for combining multiple observables at once, yes, we should be able to do this without creating N observables in the process. Just haven't had the time to implement this yet. There's a ticket about that: raquo/Airstream#18

Nikita Gazarov
@raquo
Actually, what exactly do you use your seq for?
Iurii Malchenko
@yurique
I don’t think I do anymore, it was a while ago

regarding the combine — I’m not sure I understand (but I’m not insisting here :) )

but wouldn’t this

    def combineWithMap[B, C](otherSignal: Signal[B])(project: (A, B) => C): Signal[C] = 
      new CombineSignal2[A, B, C](
        parent1 = s,
        parent2 = otherSignal,
        combinator = CombineObservable.guardedCombinator(project)
      )

behave exactly like this

    def combineWithMap[B, C](otherSignal: Signal[B])(project: (A, B) => C): Signal[C] = 
      s.combineWith(otherSignal).map { case (a, b) => project(a, b) }

?

Iurii Malchenko
@yurique
where the latter would get expanded into this
      new CombineSignal2[A, B, C](
        parent1 = s,
        parent2 = otherSignal,
        combinator = CombineObservable.guardedCombinator((_, _))
      ).map { case (a, b) => project(a, b) }

though I think I’m starting getting it :)

project will be called at most once per incoming event

Here

      new CombineSignal2[A, B, C](
        parent1 = s,
        parent2 = otherSignal,
        combinator = CombineObservable.guardedCombinator((_, _))
      ).map { case (a, b) => project(a, b) }

tuple construction might get called more than once — (_, _) — but not the project

Nikita Gazarov
@raquo
^ yes, which is exactly why the combinator must be pure. I don't really want to introduce the concept of purity into Airstream's public API. Not requiring purity is a feature. It's easy enough to construct the CombineSignal2 class manually for those who need this optimization imo.
You can test with a custom combinator and a simple diamond case similar to what's in the docs or in the video if you're still not 100% sure what's up
Anton Sviridov
@keynmol

Actually, what exactly do you use your seq for?

I also have my own hand-rolled version of this (much worse than Yurii's), and I use it for dynamic interface generation - we have some specification that comes with the notion of "a list of specifications" built-in, so I can build a def seqField[A]: EventStream[Seq[A]] given I already know how to build a simpler EventStream[A]

Nikita Gazarov
@raquo
I see, thanks for the context!