Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .claude/references/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ The reactive system is based on a **push-pull** model with three core abstractio
sources (Value) it reads from.
- **Dependents**: Each reactive source maintains a list of downstream consumers
that depend on it. When a source invalidates, it notifies all dependents.
- **ReactiveEnvironment**: Global singleton managing the reactive graph,
execution queue, and flush cycles.
- **ReactiveEnvironment**: Global singleton managing the reactive graph, the
reactive effect queue, and rounds. Its class docstring in
`shiny/reactive/_core.py` defines the terms used for the reactive system:
reactive effect queue, round, idle, cycle, and output flush.

Key implementation details:

Expand All @@ -25,8 +27,9 @@ Key implementation details:
- `Effect_()` is a side-effect that re-executes when dependencies change
- `event()` decorator suppresses reactive dependencies for specific reads
- The reactive graph is built automatically through the Context's dependency tracking
- Execution uses a priority queue to ensure correct invalidation ordering
- In tests, `reactive.flush()` forces a synchronous flush of the reactive graph
- Each round takes effects from the reactive effect queue in priority order
- In tests, `await reactive.flush()` runs rounds until the reactive environment is
idle

## Session Hierarchy

Expand Down
37 changes: 18 additions & 19 deletions shiny/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
from .html_dependencies import _page_deps
from .http_staticfiles import FileResponse, StaticFiles
from .reactive._core import _reactive_environment
from .reactive._core import on_flushed as reactive_on_flushed
from .session._session import AppSession, Inputs, Outputs, Session, session_context
from .types import MISSING, MISSING_TYPE
from .ui._page import DEPS_PLACEHOLDER, PageHtmlDocument, page_html
Expand Down Expand Up @@ -243,9 +242,9 @@ def __init__(
self._sessions: dict[str, AppSession] = {}

# Sessions with outputs, errors, or input messages to send after the next
# reactive flush (R's `appsNeedingFlush`).
self._sessions_needing_flush: dict[str, AppSession] = {}
self._unregister_flush_sessions: Optional[Callable[[], None]] = None
# round (R's `appsNeedingFlush`).
self._sessions_needing_output_flush: dict[str, AppSession] = {}
self._unregister_output_flush_hook: Optional[Callable[[], None]] = None

self._registered_dependencies: dict[str, HTMLDependency] = {}
self._dependency_handler = starlette.routing.Router()
Expand Down Expand Up @@ -474,28 +473,28 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp:
return JSONResponse({"detail": "Not Found"}, status_code=404)

# ==========================================================================
# Flush
# Output flush
# ==========================================================================
def _request_flush(self, session: AppSession) -> None:
self._sessions_needing_flush[session.id] = session
if self._unregister_flush_sessions is None:
self._unregister_flush_sessions = reactive_on_flushed(
self._flush_pending_sessions
def _request_output_flush(self, session: AppSession) -> None:
self._sessions_needing_output_flush[session.id] = session
if self._unregister_output_flush_hook is None:
self._unregister_output_flush_hook = (
_reactive_environment.on_round_finished(self._start_output_flushes)
)
_reactive_environment.request_flush()
_reactive_environment.request_round()

async def _flush_pending_sessions(self) -> None:
async def _start_output_flushes(self) -> None:
"""
After each reactive flush, send each requesting session's outputs.
After each round, start an output flush for each requesting session.

Each session sends in its own task (see `AppSession._start_flush()`), so a
slow client, or a slow `on_flush` callback, delays only its own session:
other sessions' output and the next reactive flush don't wait for it.
Each session flushes in its own task (see `AppSession._start_output_flush()`),
so a slow client, or a slow `on_flush` callback, delays only its own session:
other sessions' output and the next round don't wait for it.
"""
sessions = list(self._sessions_needing_flush.values())
self._sessions_needing_flush.clear()
sessions = list(self._sessions_needing_output_flush.values())
self._sessions_needing_output_flush.clear()
for session in sessions:
session._start_flush()
session._start_output_flush()

# ==========================================================================
# HTML Dependency stuff
Expand Down
Loading
Loading