Skip to content

Push Incremental Updates#177

Merged
seanhess merged 8 commits intomainfrom
experiment-updates
Oct 30, 2025
Merged

Push Incremental Updates#177
seanhess merged 8 commits intomainfrom
experiment-updates

Conversation

@seanhess
Copy link
Owner

@seanhess seanhess commented Oct 27, 2025

Both sockets and http have always used a request-response cycle to process actions. While an action is processing the HyperView locks and waits for a response (slightly configurable now, see #174).

UPDATE: You now CAN cancel long-running actions, making this much more useful

This PR adds PushUpdate to the Hyperbole effect, which allows you to stream incremental view updates over the socket while a long-running action is processing. This is intuitively a good thing, but there are major limitations:

  1. I had to remove the ability to handle actions over HTTP. The page still loads over HTTP of course, but the app now assumes you have a connected socket for all actions (and queues actions if disconnected)
  2. There's no way to cancel a long-running action. You're stuck until an action completes or you unload it.
  3. Actions and page loads share the Hyperbole effect, but it's meaningless to use PushUpdate in a page load. This might be confusing.

Questions:

  • Will anyone miss the ability to fall back to handling actions via HTTP when the socket is offline?
  • Should views be allowed to push updates to other views? Like trigger but directly push an update? How should conflicts work?
  • Any feedback?

Example: see the bottom of the Concurrency page in this branch

data Tasks = Tasks
  deriving (Generic, ViewId)

instance (Debug :> es) => HyperView Tasks es where
  data Action Tasks
    = RunLongTask
    | Interrupt
    deriving (Generic, ViewAction)

  type Concurrency Tasks = Replace

  update RunLongTask = do
    forM_ [1 :: Float .. 100] $ \n -> do
      pushUpdate $ taskView (n / 100)
      delay 50
    pure $ taskView 1
  update Interrupt = do
    pure $ col ~ gap 10 $ do
      el "Interrupted!"
      taskView 0

taskView :: Float -> View Tasks ()
taskView pct = col ~ gap 10 $ do
  taskBar

  if isRunning
    then button Interrupt ~ btn $ "Interrupt"
    else button RunLongTask ~ btn . whenLoading disabled $ "Run Task"
 where
  isRunning = pct > 0 && pct < 1
  taskBar
    | pct == 0 = el ~ bg Light . pad 5 $ "Task"
    | pct >= 1 = row ~ bg Success . color White . pad 5 $ el $ text "Complete"
    | otherwise = progressBar pct "Task"

TODO:

  • pushUpdateTo isn't correctly setting the ViewId. Needs testing / example.
  • works with thrown exceptions
  • Edge case testing - what if sockets are offline, and requests are queued? Does concurrency locking work properly?

@seanhess
Copy link
Owner Author

The framework now automatically cancels any running actions that match the same client and HyperView id. These can't occur unless Concurrency has already been set to Replace.

So for type Concurrency id = Drop, the client drops any new requests, and nothing happens, while for Replace, it sends the request, which cancels the old one.

@seanhess
Copy link
Owner Author

There are still a few strange edge-cases to the concurrency stuff, but it doesn't need to hold up this PR

@seanhess seanhess merged commit 173c610 into main Oct 30, 2025
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant