-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert bindAll
to an async function
#3904
Conversation
Would |
In addition to
And we'd also need sync/async versions of the functions that have been touched in this PR. It might be simpler to find packages that use |
I don't think the addition of That said, I'm not totally against changing |
Could Another thought that's less likely to pan outThe other vague intuition I had while debugging this issue is that there's both asynchronous programming and recursion at play. It might have been a detail particular to the example we were looking at, but I found myself wondering if the Something else we might want to look at: we protect ourselves from the double bind in |
That's true, it won't add too much complexity. However, something that we should think about is how to deal with bindings that can only be implemented with async versions of these functions. In terms of sync/async
For example, in Shinylive (for both R and Python), The category that would have problems is (3). Also note that if there are any bindings that currently are in category (1) but do async loading of resources, they won't work in Shinylive. After writing this all out, part of me thinks that we should have the separate sync/async methods, but part of me also just wants to rip off the band-aid and make everything async so that we don't have to maintain parallel code paths indefinitely into the future.
Yes, for symmetry, I think it would make sense to mark |
Unfortunately, no... The main issue is that when With all the async/promise stuff going on under the hood, the only (hypothetical) way to guarantee that everything has completed when
If I'm understanding correctly, I think that these two possibilities don't quite get to the root of the problem -- the double binding of an output is really more of a symptom of a deeper problem, which is that, currently, the code that handles the bindings is not guaranteed to execute in any particular order because we launched a Promise and didn't wait for it to finish. Because the code is not guaranteed to execute in a particular order, it becomes very hard to reason about, and multiple concurrent "tasks" could be doing overlapping operations on the same data structures, which could violate assumptions about how the code is supposed to work. Making it fully async-await will guarantee that execution order is linear. |
Thanks for considering my comments and for your replies, @wch. I think the fact that To clarify my above points: if Personally, I'm in favor of making From a quick scan of GitHub search results, I think most people are already calling |
This closes #3899, but is currently just a draft PR because there's an issue that needs to be addressed with it.
The problem was the following:
onValueChange
is anasync
function, but there was one place where it was getting called without anawait
. This allowed multiple calls torenderValue
to happen concurrently, which caused the bad behavior.This PR addresses the issue by converting that call to
onValueChange()
toawait onValueChange()
, and making all the functions on the call stack alsoasync
, and usingawait
to call all of them.The result of all this is that the externally-facing function
Shiny.bindAll
has changed from sync to async. Overall, this is an improvement from Shiny 1.7.5, because the code that's called from that function has been fixed so that all async functions are awaited. However, compared to Shiny 1.7.4, it could still be a step back because in that version, the caller ofbindAll
could expect that function to work synchronously -- all of its effects would be completed when the function returns.One possible way of dealing with this is to do the same pattern that we used in
render.ts
, where we kept old functions likerenderHtml
and provided new functions with the suffixAsync
, likerenderHtmlAsync
. If we do this, then we would end up with a new function calledbindAllAsync
(along with some others that would be on the call stack). We could also deprecatebindAll
and encourage extension authors to usebindAllAsync
.Another possibility is to leave the code as it is in this PR, and tell extension authors that
bindAll()
is now an async function.One other note: after enabling the
no-floating-promises
eslint rule, I found that there were a number of other places where we create promises without awaiting them, and those should be addressed in another PR.