feat: Add hard=True to session.close() for terminal session ends#2271
Draft
elnelson575 wants to merge 1 commit into
Draft
feat: Add hard=True to session.close() for terminal session ends#2271elnelson575 wants to merge 1 commit into
hard=True to session.close() for terminal session ends#2271elnelson575 wants to merge 1 commit into
Conversation
Adds an opt-in hard-disconnect path to `session.close()` and a matching `hard_disconnect_message` argument on `App` for the default closed-overlay text. Hard close performs three coordinated tiers: 1. Sends a `hardDisconnectConfig` custom message carrying the closed- overlay text (resolved as per-call `message` → app default → `"This app has closed."`). 2. Closes the websocket with code `4001` and reason `shiny-hard-disconnect` so hosting platforms can recognize "release this worker without holding for reconnect." 3. Additionally clears the root-level `input`, `_downloads`, `_dynamic_routes`, and `_message_handlers` collections that a soft close leaves in place. `on_ended` callbacks fire before the hard cleanup, so teardown code can still read session state. Default behavior is unchanged: existing callers of `session.close()` with no arguments behave exactly as today. The matching client-side handling (4001 recognition, `hardDisconnectConfig` custom-message handler, distinct closed-state overlay) lives in R shiny's srcts and reaches py-shiny when shiny.js is vendored from a release that includes it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in hard-disconnect path to
session.close()for ending a session terminally from server code — typically in response to a user action like Submit or Log Out:A hard close performs three coordinated tiers:
hardDisconnectConfigcustom message carrying the closed-overlay text, then closes the websocket with code4001/ reasonshiny-hard-disconnect. Hosting platforms (Shiny Server, Posit Connect) can recognize this as "release the worker without holding for reconnect."input,_downloads,_dynamic_routes, and_message_handlers.on_endedcallbacks fire before this cleanup, so teardown code can still read session state.4001, stashing thehardDisconnectConfigmessage, rendering a distinct closed-state overlay, firingshiny:closed) lives in R Shiny'ssrcts/and reaches py-shiny whenshiny.jsis vendored from a release that includes it. Until then the server side runs as designed; the browser still sees today's grey "disconnected" overlay.App-level default text is configurable:
App(hard_disconnect_message="…"). Fallback chain: per-callmessage→ app default →"This app has closed.".Backward compatibility: existing callers of
session.close()with no arguments are unaffected — soft close is byte-for-byte unchanged.This is the py-shiny port of "Plan A" from the upstream hard-disconnect design (R shiny
feat/hard-disconnect). The idle-timeout variant ("Plan B") is out of scope for this PR.Changes
shiny/_app.py— newhard_disconnect_message: str | Nonekwarg onApp.__init__shiny/session/_session.py—Session.close()gainshardandmessagekwargs;AppSession.close()implements the hard path;SessionProxy.close()forwards; newAppSession._hard_close_cleanup(); new module-level constantsHARD_DISCONNECT_CLOSE_CODE,HARD_DISCONNECT_REASON,HARD_DISCONNECT_DEFAULT_MESSAGEshiny/express/_stub_session.py— signature paritytests/pytest/test_hard_disconnect.py— 13 unit testsCHANGELOG.md— entry under[UNRELEASED]Test Plan
pytest tests/pytest/test_hard_disconnect.py— 13 passedpytest tests/pytest/test_destroy.py tests/pytest/test_hard_disconnect.py— 88 passed (no regression in adjacent destroy/teardown tests)pytest tests/pytest/ --ignore=tests/pytest/test_theme.py— 698 passed (theme tests needlibsass, pre-existing local env issue)pyright shiny/session/_session.py shiny/express/_stub_session.py tests/pytest/test_hard_disconnect.py— 0 errorsblack/isortclean on changed filesOut of scope
hard_disconnect_after) — separate follow-upsrcts/, arrives viamake upgrade-html-deps