pkamenarsky / concur-replica

Server-side VDOM UI framework for Concur
BSD 3-Clause "New" or "Revised" License
139 stars 20 forks source link

How to update widget based on timeout? #47

Closed thalesmg closed 3 years ago

thalesmg commented 3 years ago

Hello!

First, thanks for this awesome lib!! :beers:

I was wondering: how one could update some widget based on a timeout? For example, if one is trying to make a clock widget, or simply refresh the widget contents periodically?

I was considering, as a workaround, to (somehow) use setInterval to set up a timer that would periodically trigger a custom event on the given element. I'd still have to figure out how to add the required javascript to the page, and also how to reference the widget (by id, probably).

Is there some easier and/or more idiomatic way to trigger time based events on concur-replica?

Cheers

pkamenarsky commented 3 years ago

Yeah, that one is easy 😄 Something like:

timeoutWidget = do
  text "1" <|> liftIO (threadDelay 1000000)
  text "2" <|> liftIO (threadDelay 1000000)
  text "3" <|> liftIO (threadDelay 1000000)

should do the trick. Or even:

timeoutWidget2 = do
  div [] [ text "1", liftIO (threadDelay 1000000) ]
  div [] [ text "2", liftIO (threadDelay 1000000) ]
  div [] [ text "3", liftIO (threadDelay 1000000) ]

(Beware of type errors, code is untested).

Cheers!

thalesmg commented 3 years ago

Wow, I didn't notice that threadDelay could be simply lifted like this! Thanks! :beers:

But I tried the following code:

clock = forever $ do
  now <- liftIO getCurrentTime
  div [] [text . T.pack . show $ now, liftIO $ threadDelay 1000000]
  text "other text" <|> liftIO (threadDelay 3000000)

It does print out the current time for 1 second (but not other text), then it prints other text and the time disappears, then everything disappears.

Due to forever, shouldn't it keep rendering at least the time and other text loop?

EDIT: I've just tried another variation:

clock = forever $ do
  now <- liftIO getCurrentTime
  div [] [text . T.pack . show $ now, liftIO . forever $ threadDelay 1000000]
  text "other text" <|> liftIO (forever $ threadDelay 3000000)

This time, the text stays rendered on the page, but the current time is never recomputed and other text never shows up.

thalesmg commented 3 years ago

By the way, your example works fine: it prints 1, 2, 3 and then stops. If I add forever or simply recurse, it prints the numbers in a loop.

I'm still trying to figure out how is this working and why other text from my example never prints out. :thinking:

thalesmg commented 3 years ago

I got the clock example working, despite not understanding why there can't be more elements after the timeout one :stuck_out_tongue_closed_eyes:

clock = do
  now' <- liftIO getCurrentTime
  div [] [text . T.pack . show $ now', liftIO $ threadDelay 1000000]
  clock

This one updates the time each second, as long as I don't add any other elements after the div.

pkamenarsky commented 3 years ago

Interesting, your clock example with forever works here, see attached video.

I suspect the explanation is that concur has some concurrency bugs still, which I haven't had the time to fix. Unfortunately, due the nature of the concur model it's not that easy to get everything right, so this will take time :(

I wouldn't recommend using it in production.

time.zip

thalesmg commented 3 years ago

I see! Maybe it is something platform dependent? I'm using Linux, and you seem to be using MacOS.

I wouldn't recommend using it in production.

I was just trying it for a small personal project. I found it while researching for Phoenix LiveView alternatives. =)

I'm having trouble trying to understand concur's Widget/Free (SuspendF v) a type and how it all wires up together, it seems very complex (without many comments to help :sweat_smile: ).

I managed to make some text appear after the clock, but not sure why the second version works and the first doesn't:

clock = do
  now' <- liftIO getCurrentTime
  div [] [(text . T.pack . show $ now') <|> liftIO (threadDelay 1000000)]
  clock

-- This does not print the text after the clock
clockAndStuff = do
  div [] [clock]
  p [] [text "I should print!"]

-- This prints the text after the clock
clockAndStuff2 = do
  div [] [clock, p [] [text "I should print!"]]

Anyway, thanks a lot for the help!! I can continua playing with the lib now! Cheers! :beers:

pkamenarsky commented 3 years ago

I'm having trouble trying to understand concur's Widget/Free (SuspendF v) a

It's a bit convoluted, but the basic idea is to describe an UI in terms of "show a view" and "block on IO" steps, then the concur magic (e.g. executing Widgets in parallel and killing all siblings when a Widget ends) happens in orr :)

Your examples work as intended:

clockAndStuff = do
  div [] [clock]
  p [] [text "I should print!"]

Here, clock never ends, so div [] [clock] never ends and p [] [text "I should print!"] is never reached. On the other hand:

clockAndStuff2 = do
  div [] [clock, p [] [text "I should print!"]]

Here you're displaying the clock and text in parallel, so both show at the same time. Hope this helps 👍

thalesmg commented 3 years ago

It does help! Thanks for the explanation!! =)

I'll still need to pour some thinking into the whole thing :see_no_evil: , but now I see why each example behaves differently.

As a small complementary question, what is the difference between StepBlock and StepIO?

They both have the same structure:

  | forall r. StepBlock (IO r) (r -> next)
  | forall r. StepIO    (IO r) (r -> next)

But, inspecting stepW, it seems that the purpose of StepIO is to execute the longest sequence of IO actions queued up, while StepBlock seems to execute the single next IO action and then immediatly yield a view plus the suspened state:

stepW :: v -> Free (SuspendF v) a -> IO (Either a (v, Maybe (Either (STM (Free (SuspendF v) a)) (IO (Free (SuspendF v) a)))))
stepW _ (Free (StepView v next))  = stepW v next
stepW v (Free (StepIO a next))    = a >>= stepW v . next
stepW v (Free (StepBlock a next)) = pure $ Right (v, Just $ Right (a >>= pure . next))
stepW v (Free (StepSTM a next))   = pure $ Right (v, Just $ Left (a >>= pure . next))
stepW v (Free Forever)            = pure $ Right (v, Nothing)
stepW _ (Pure a)                  = pure $ Left a

Is this understanding correct?

Cheers!

pkamenarsky commented 3 years ago

Your understanding is indeed correct!

StepIO is used for "non-blocking" IO actions, e.g. thinks like newIORef, which don't yield a view and don't need to wait on an outside action to unblock. On the other hand, StepBlock is used for blocking IO (threadDelay, or user events, like mouse clicks or key presses), and indeed liftIO lifts an IO action into StepBlock, because we don't have any guarantee that that action is non-blocking.

Think of StepIO as an optimisation more than anything else.

Since you're showing interest in Concur, make sure to check out the original framework (created by @ajnsit) here: https://github.com/ajnsit/concur, and its documentation site, which has a really well written and thorough introduction to Concur's mode:l https://github.com/ajnsit/concur-documentation/blob/master/README.md.

thalesmg commented 3 years ago

Cool! Thanks for all the explanations and for the references! I'll check those docs out! :tropical_drink: