Circuit can be paused by Blazor when inactivity is detected - #67098
Conversation
…us because only loosing focus saves content of elements with bindings to server.
…is opened but happens after it gets closed.
…related to SignalR - clean unnecessary code.
…across the tests.
…onPauseRequested`
…to-pause config 2) with server runtime (with circuit=undefined). Do not let the 2nd pass overwrite config to undefined.
|
When fixing the problem with resuming expired client-stored state (#64798), I proposed to Javier that we could add a separate property in Currently, we are computing the expiration period as the maximum of Now with pausing becoming a much more prominent feature, I would like to evaluate the question again. How do you feel about adding something like |
I'm not sure what we are gaining from it. And it would imply even more complexity as we now would need to harmonize three separate settings. The server is always in control of what state is valid, and if we are already reloading when resume fails, I'm not sure what we gain from it. Dropping the state on the client while is still usable is the worst thing that can happen on this feature, it breaks the session continuity, and in the case of a graceful pause, it introduced an issue that didn't exist before. |
What if the user wants short server-side retention period because they have limited server resources but they do not care how long the client holds the paused state? They would need an independent knob to turn. |
Application configures the timeout for pause as opt-in feature. This value is treated as a suggestion for the framework, depending on UI state, deferral or rejection of pause can happen.
Some features are not available on all browsers. In this case [❌ browser name] is added to mark that they are not affected.
Pause deferral / rejection
Scenarios where auto pause according to
hiddenDelayMillisecondsis not what we want. Some scenarios work as "veto" - e.g. edited input will stay edited until user comes back to it and cleans it manually, and some scenarios work as "deferral" - a job that is supposed to complete and should be awaited.DotNetStreamReference) [deferral]JSStreamReference) [deferral]IBrowserFilecircuit owned streams [deferral]text/search/email/url/tel/password/number,textarea, content editable div,InputText) which are edited and focused. Edited == its content differs from the initial value sent by the server - similar to React's "dirty" field concept. [veto]iframethat contains any focused, edited HTML element from the previous point [veto]In all these scenarios
AutoPauseManagertracks the active work and waits until it is resolved, on the top of waiting forhiddenDelayMillisecondsto elapse. If visibility changes tovisibleduring this wait, the pause doesn't happen.Inputs that do not require blure/focus-out to preserve values: radio, checkbox, select, slider, date/time/datetime-local/month/week.
Risks:
input.filesis read-only. Attempt to bind to it and restore it from persistance storage would cause browser exception. Bytes are saved at the server but<input type=file>won't know it after the resume. Loss of data on the client is accepted - developer can manually write e.g.ToDo: add this snippet to documentation with description of not-supported scenario from
Pause_BindWithPersistentStateOnFileInput_ThrowsInvalidStateError.@binding, their focus out didn't save the values to server and the state is lost after tab change. We consider it by app design - custom element is responsible for saving the values. If the button was not pressed, it's okay to loose it.<canvas>do not haveactiveElement, drawing component state cannot be saved, we cannot check when it's "dirty" and we don't have an easy way to serialize it. It's the app responsibility to veto pause viaBlazor.pause.waitForor running autosave on each stroke.<iframe>and popups, from security reasons framework has no access to it, same mitigation as above.RTCPeerConnectionand gets notified about its creation/destruction through internal callbacks. Monkey-patchingRTCPeerConnectionconstructor is a risky change we would like to avoid. Mitigation: app tracks active connections and saves state in aBlazor.pause.waitForhandler.MediaStreamManager. The only way to mimic it would be to monkey-patch its media getters - not acceptable. Mitigation: app that callsgetUserMediais responsible for saving state in aBlazor.pause.waitForhandler as long as the stream is alive.XRSessionis internal to chromium. Mitigation: app that opens the session should save state in aBlazor.pause.waitForhandler. [Safari ❌, Firefox ❌]Other
Cases independent from SignalR connection but chromium defers freeze for them:
fetchrequest is handed from renderer process that chromium freezes. However, pause influences SignalR only that is independent fromfetch.fetch.fetch[Safari ❌, Firefox ❌].Public API
C# Server Configuration
Auto-pause is configured by calling
AddAutoPauseon the circuit'sBrowserOptions, which flow to the client:Registration extension (same namespace). Calling AddAutoPause is what enables the feature:
Used via query string in tests (
auto-pause=true&auto-pause-delay-ms=200) or viaAddAutoPausein production apps:JS API
Blazor.pause.waitForregisters a callback invoked before pause. The callback receives anAbortSignalthat aborts if the tab becomes visible (cancelling the pause). Use it to save state or veto pause by holding a long-running promise.The same API serves every pause source, routed by the optional
source: An untagged handler runs on every kind of pause. A handler registered with{ source: 'server' }runs only on a server-initiated pause (Circuit.RequestCircuitPauseAsync). Any other label (the auto-pause package uses'auto') runs only on client/auto pauses. This replaces the previousCircuitStartOptions.onPauseRequestedcallback, which is removed: server-pause deferrals are now registered the same way as every other deferral, just scoped with{ source: 'server' }.Fixes #64886