diff --git a/.claude/references/architecture.md b/.claude/references/architecture.md index 265432d77..6550db755 100644 --- a/.claude/references/architecture.md +++ b/.claude/references/architecture.md @@ -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: @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d1678a4c9..843055959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Breaking changes + +* Reactive effects now run concurrently, following Shiny for R's model. A reactive flush starts each invalidated effect and no longer waits for an `async` effect's awaited part, so a slow `async` effect, calc, or render function no longer delays other sessions. Within a session, input changes and `reactive.invalidate_later()` still wait until all of that session's effects have finished, and outputs are sent together once they have. What app authors may notice: + + * `async` effects interleave at each `await` instead of running one after another. `priority` orders when effects start, not when they finish. Runs of the same effect still don't overlap: a re-run waits for the previous run. + + * `reactive.lock()` no longer pauses reactive processing: Shiny itself no longer takes it. To change reactive state from another `asyncio` task, set the value directly (a flush is scheduled automatically) and `await reactive.flush()` to wait until the resulting reactive work has finished. + + * `await reactive.flush()` returns right away, without waiting for dependents, when called from within an effect or from a task started by an effect that is still running, since waiting there could deadlock. A task that outlives the effect that started it, such as an extended task's body, still waits. + + * Message handlers (`session.set_message_handler()`) run in their own task, so a slow handler no longer holds up other messages from the client. + + * Each session sends its outputs in a task of its own, once all of its effects have finished. A slow client, or a slow `session.on_flush()` callback, delays only its own session's output, not other sessions' or the next reactive flush. + + * Setting a `reactive.value` schedules a flush, including when `set()` is called from another thread. The reactive graph itself still isn't thread-safe, so from another thread use `loop.call_soon_threadsafe(value.set, new_value)`. + + (#2508) + ### New features * `ui.sidebar()` gains a `role` parameter (`"form"`, `"search"`, `"complementary"`, or `"region"`) for opt-in ARIA landmark markup (rstudio/bslib#1359). `"complementary"` renders an `