Skip to content

Commit d471494

Browse files
authored
feat(session): Add session.allow_reconnect() (#2441)
1 parent 8e1e605 commit d471494

12 files changed

Lines changed: 155 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
* `playwright.controller.OutputTextVerbatim` is deprecated alongside `ui.output_text_verbatim()` and now emits a `ShinyDeprecationWarning` when constructed. Please use `playwright.controller.OutputCode` instead. (#2097)
1515

16+
### New features
17+
18+
* Added `session.allow_reconnect()`, the Python counterpart to Shiny for R's `session$allowReconnect()`. Call it with `True` to let the browser reconnect to its session (showing a countdown dialog instead of the "Disconnected from server" overlay) when the hosting environment keeps sessions alive after a client disconnects, or with `"force"` to attempt the reconnect anywhere. (#2441)
19+
1620
### Improvements
1721

1822
* The README and the `shiny skills` CLI help now explain that [`library-skills`](https://library-skills.io) must be run from your own project directory, since it installs the bundled Agent Skills of the packages that project has installed. The previous wording left that precondition implicit, so running the command from an empty directory or from a clone of py-shiny silently installed nothing. (#2447)

docs/_quartodoc-core.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ quartodoc:
296296
- session.Session.on_flushed
297297
- session.Session.on_ended
298298
- session.Session.dynamic_route
299+
- session.Session.allow_reconnect
299300
- session.Session.close
300301
- input_handler.input_handlers
301302
- kind: page
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from shiny import App, Inputs, Outputs, Session, render, ui
2+
3+
app_ui = ui.page_fluid(
4+
ui.markdown("""
5+
This app lets the browser reconnect after its connection to the server
6+
drops. Use the button below to close the connection: instead of the
7+
usual "Disconnected from server" overlay, you should see a countdown
8+
dialog while the client reconnects, and the counter below should keep
9+
working once it does.
10+
11+
`"force"` is used here so the reconnect is attempted even when the
12+
hosting environment does not support resuming sessions. Use `True` in
13+
production so the client only reconnects where the session is actually
14+
kept alive.
15+
"""),
16+
ui.input_action_button(
17+
"close", "Close the connection", onclick="Shiny.shinyapp.$socket.close()"
18+
),
19+
ui.input_action_button("count", "Count"),
20+
ui.output_text("counter"),
21+
)
22+
23+
24+
def server(input: Inputs, output: Outputs, session: Session):
25+
session.allow_reconnect("force")
26+
27+
@render.text
28+
def counter():
29+
return f"Clicked {input.count()} times."
30+
31+
32+
app = App(app_ui, server)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from shiny.express import input, render, session, ui
2+
3+
ui.markdown("""
4+
This app lets the browser reconnect after its connection to the server
5+
drops. Use the button below to close the connection: instead of the usual
6+
"Disconnected from server" overlay, you should see a countdown dialog while
7+
the client reconnects, and the counter below should keep working once it
8+
does.
9+
10+
`"force"` is used here so the reconnect is attempted even when the hosting
11+
environment does not support resuming sessions. Use `True` in production so
12+
the client only reconnects where the session is actually kept alive.
13+
""")
14+
15+
ui.input_action_button(
16+
"close", "Close the connection", onclick="Shiny.shinyapp.$socket.close()"
17+
)
18+
ui.input_action_button("count", "Count")
19+
20+
session.allow_reconnect("force")
21+
22+
23+
@render.text
24+
def counter():
25+
return f"Clicked {input.count()} times."

shiny/session/_session.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,46 @@ async def close(self, code: int = 1001) -> None:
260260
Close the session.
261261
"""
262262

263+
@add_example(example_name="session_allow_reconnect")
264+
def allow_reconnect(self, value: bool | Literal["force"]) -> None:
265+
"""
266+
Allow the client to reconnect to a session after a disconnect.
267+
268+
By default, when the websocket connection between the browser and the server
269+
drops, Shiny displays the "Disconnected from server" overlay and the client
270+
gives up. Calling this method with ``True`` tells the client to instead show a
271+
countdown dialog and attempt to reconnect to its session.
272+
273+
On a successful reconnect, the browser sends all of its current input values to
274+
the session on the server, and the server recalculates any outputs and sends
275+
them back to the client.
276+
277+
Parameters
278+
----------
279+
value
280+
One of the following:
281+
282+
* ``True``: allow the client to reconnect after a disconnect, but only when
283+
running in a hosting environment (such as Posit Connect or Shiny Server)
284+
that has reconnections enabled.
285+
* ``False``: do not allow the client to reconnect. This is the default.
286+
* ``"force"``: always attempt to reconnect, regardless of what the hosting
287+
environment reports.
288+
289+
Note
290+
----
291+
Reconnecting requires the server to keep the session alive after the client
292+
disconnects, which is a feature of the hosting environment rather than of Shiny
293+
itself. ``"force"`` exists for testing on a local connection: the client will
294+
try to reconnect anywhere, but on a plain ``shiny run`` server the attempt
295+
starts a brand new session rather than resuming the old one.
296+
"""
297+
if value is not True and value is not False and value != "force":
298+
raise ValueError(
299+
f'`value` must be `True`, `False`, or `"force"`, not {value!r}.'
300+
)
301+
self._send_message_sync({"allowReconnect": value})
302+
263303
@abstractmethod
264304
def _is_closed(self) -> bool:
265305
"""

shiny/www/shared/_version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"note!": "Generated by scripts/htmlDependencies.R: do not edit by hand",
33
"package": "shiny",
4-
"version": "1.14.0.9000 (rstudio/shiny@ef93041ff7cbef679dbc37f527e09427eaf8d80a)"
4+
"version": "1.14.0.9000 (rstudio/shiny@d19095f4b3dd2b699406db2693c487fb38bd2d4a)"
55
}

shiny/www/shared/bootstrap/_version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"note!": "Generated by scripts/htmlDependencies.R: do not edit by hand",
3-
"shiny_version": "1.14.0.9000 (rstudio/shiny@ef93041ff7cbef679dbc37f527e09427eaf8d80a)",
3+
"shiny_version": "1.14.0.9000 (rstudio/shiny@d19095f4b3dd2b699406db2693c487fb38bd2d4a)",
44
"bslib_version": "0.12.0.9000 (rstudio/bslib@97aa1abc262bff3fdfab686d4206a45fe7276ad6)",
55
"htmltools_version": "0.5.9.9000 (rstudio/htmltools@5135d17923ab80b3599da1f335d5d33eb9aa63a3)",
66
"bootstrap_version": "5.3.8"

shiny/www/shared/shiny.js

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/shared/shiny.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/shared/shiny.min.js

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)