fix(event): send a failed handler's writes after its last yield

FA
14h ago
+173-6

Related to #7248. Pairs with #7412.

Problem

A foreground handler can change state after its last yield, for example in an except asyncio.CancelledError block when a supersedes=True call cancels it, or just before it raises. Nothing sends those changes to the frontend.

  • Memory and disk send them late, with the next handler's first delta. The next handler gets the same state object, and that object still marks the vars as dirty.
  • Redis without a cached lease (the default, REFLEX_OPLOCK_ENABLED=False) never sends them. The next handler unpickles a fresh copy, and _init_bookkeeping clears the dirty marks. The frontend shows the old value until the var changes again or the page reloads.

Fix

When process_event raises or is cancelled, _execute_event flushes the delta with chain_updates(None, root_state=...) before the exception propagates. The flush runs under the state lock, so it reaches the frontend before any delta from the next handler. If the flush fails, it is logged and the handler's own exception propagates. The background branch uses the same pattern.

Background handlers are not changed. They already flush in their finally.

Tests

  • test_failed_handler_emits_writes_after_last_yield: memory, disk and redis, each cancelled and raising. All 6 cases fail on main and pass with the fix.
  • test_failed_handler_flush_error_keeps_handler_exception: a computed var makes the flush fail. The handler's RuntimeError is reported and the flush error is logged. It fails when the except in _flush_failed_handler is removed.

Notes

  • On Redis without #7412, the frontend now receives these changes, but Redis still drops them. #7412 fixes the storage side.
  • #7412 adds a docs sentence in docs/events/yield_events.md about this late delivery. Remove that sentence after this PR merges.