Skip to content

Conversation

@JovanGerb
Copy link
Contributor

@JovanGerb JovanGerb commented Oct 30, 2025

This PR defines RefreshComponent, which allows you to have a refreshing HTML display, which updates whenever the given RefreshTask returns a new HTML.

For cancellation, we store an Option IO.CancelToken. This could be generalized to storing an arbitrary IO Unit function, but I don't think that would be very useful.

@dranov
Copy link

dranov commented Dec 1, 2025

In Veil, we have a need for widgets where the data is provided incrementally. Do you intend to merge this (or a similar solution) in the near future?

@JovanGerb
Copy link
Contributor Author

I just opened the demo file from this PR, and did the countToTen demo, and as you can see whenever I reopen the infoview, the numbers flicker a bit. This is what I mean with the glitching.

WidgetGlitch.mp4

catch e =>
if let .internal id _ := e then
if id == interruptExceptionId then
return .last <| .text "This component was cancelled"
Copy link
Collaborator

@Vtec234 Vtec234 Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am seeing this in the countToTen demo. To reproduce, uncomment line 33, then check that the counter on line 30 is running, then uncomment line 36. Now line 30 is cancelled. It seems worrying since no shared cancel token is explicitly created here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha interesting! Although it's true that no cancel token is created explicitly, we are implicitly passing around the cancel token that was given to the command in question.

Apparently this cancel token from line 30 gets set when (un)commenting line 36. I find this a bit surprising. I'd have to look more carefully into how/where the cancel tokens usually get passed around.


end MonadDrop

instance : Lean.Server.RpcEncodable Unit where
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this just be From/ToJson? We usually don't impl RpcEncodable directly for types that don't do something with RPC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this is that I want to be able to have an RPC method that doesn't return anything, called from JavaScript. And the @[server_rpc_method] attribute requires the RpcEncodable instance. Alternatively I'd have to return a String or something else, and then throw it away.

Warning: the thread that is updating `state` has started running on the Lean server before the
widget is activated. The advantage is that there is no delay before the computation starts.
The disadvantage is that if a `RefreshComponent` is reloaded/unloaded too quickly,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdym by 'reloaded/unloaded' here? Is the situation just that a tactic that saves this widget (and launches the thread) is elaborated, then we make an edit, and it gets re-elaborated while leaving the previous thread alive? If so, is it not possible to reuse the standard tactic cancellation mechanism? E.g. what happens if you pass the CoreM cancel token instead of one created manually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the description to be more clear. The issue is when clicking on expressions in the goal, every shift-click generates a call to mkGoalRefreshComponent. When shift-clicking as fast as possible, there is not enough time for each of the intermediate widgets to load.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is when clicking on expressions in the goal, every shift-click generates a call to mkGoalRefreshComponent.

Ah, if doing that you could also create/cancel cancellation tokens in the outer function (e.g. in cycleSelections). Then you probably wouldn't need the global cancel token map.

Copy link
Collaborator

@Vtec234 Vtec234 Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. what happens if you pass the CoreM cancel token instead of one created manually?

I'm still wondering about this. In particular w.r.t. to "when the infoview gets closed, cancelRefresh isn't run, and I don't know how to fix this." Using the CoreM token wouldn't cause anything to happen when the infoview is closed, of course, but I think we don't care about that. Mainly, we should ensure that no more than one RefreshComponent thread chain is alive at any given time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my solution with storing the cancel token in a IO.Ref ensures exactly that: whenever a new RefreshComponent thread is started, it cancels the previous one. So there can be at most one alive at any given time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could also create/cancel cancellation tokens in the outer function (e.g. in cycleSelections).

The function cycleSelections is not the outer function. It is called every time when the props : PanelWidgetProps is updated. In particular when selecting something, props.selectedLocations is updated.

Instead, the outer function is the one generated by

@[widget_module]
def cycleComponent : Component PanelWidgetProps :=
  mk_rpc_widget% cycleSelections

But I haven't looked at the code implementing widget_module and mk_rpc_widget%.

Copy link
Collaborator

@Vtec234 Vtec234 Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, the outer function is the one generated

Right, but your RPC method gets called every time the props change; so you can put an IO.Ref in the props (by extending PanelWidgetProps) and modify it as needed. I'm just suggesting this to avoid the situation where all widgets corresponding to the given declaration get cancelled globally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. This would allow multiple copies of the same widget to co-exist, because each of them can have its own canceltoken.

Though I don't see how the current approach would ever have all widgets corresponding to the given declaration get cancelled globally.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did I misunderstand

If the same widget is loaded again, we set the previous cancel token.
As a result, there can be at most one instance of that particular widget
running at a time, and any other instances will say This component was cancelled.

? You use decl_name% as the key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every new widget cancels the previous one, so for any given key there can be at most 1 computation running.

But that one computation wouldn't get cancelled, unless the corresponding widget is unloaded.

@JovanGerb
Copy link
Contributor Author

For the flickering that can be seen in the video, when the message disappears, this is actually the <></> that is the initial/default value for the HTML (which can be seen at the end of the RefreshComponent implementation in TypeScript).

In an earlier version of this widget, instead of disappearing for a bit, the message would change into whatever the initial HTML was set to, which gave a more jarring effect.

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.

3 participants