Skip to content

[Blazor] Adds support for pausing and resuming circuits with support for pushing the state to the client #62271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: javiercn/persistent-component-state-ungraceful
Choose a base branch
from

Conversation

javiercn
Copy link
Member

@javiercn javiercn commented Jun 6, 2025

Missing:

  • Tests/E2E tests
  • UX for the reconnection
  • Template UI changes to the reconnection UI
  • Triggering the pause from the server.

@javiercn javiercn marked this pull request as ready for review June 6, 2025 19:20
@javiercn javiercn requested a review from a team as a code owner June 6, 2025 19:20
@@ -14,7 +14,7 @@
<HeadOutlet />
</head>
<body>
<Routes />
<Routes @rendermode="InteractiveServer" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore the sample changes. I'll undo them

@javiercn javiercn force-pushed the javiercn/persistent-component-state-graceful branch from dcfe665 to 4ae47fd Compare June 8, 2025 22:12
Comment on lines +39 to +42
public void PauseCircuit()
{
_circuitHost.TriggerCircuitPause();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method just be called Pause, since it's containing type is already Circuit, so it's sort of implied that the circuit is the thing being paused?

Comment on lines +34 to +44
if (saveStateToClient)
{
await SaveStateToClient(circuit, store.PersistedCircuitState, cancellation);
}
else
{
await circuitPersistenceProvider.PersistCircuitAsync(
circuit.CircuitId,
store.PersistedCircuitState,
cancellation);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading this section of code, it wasn't obvious to me that SaveStateToClient actually saves state on the server as a fallback. Do you think it would be clearer if SaveStateToClient was actually TrySaveStateToClient, and the fallback logic is just what's currently in the else block here? Something like:

if (saveStateToClient && await TrySaveStateToClient(...))
{
    // State successfully saved to the client - nothing left to do on the server
}
else
{
    await circuitPersistenceProvider.PersistCircuitAsync(...);
}

Comment on lines +69 to +70
// Root components descriptors are already protected and serialized as JSON, we just convert the bytes to a string.
var rootComponents = Encoding.UTF8.GetString(state.RootComponents);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid encoding/decoding root component state by serializing it directly to a string rather than to a byte[] first via PersistedComponentState.PersistAsJson()?

Comment on lines 318 to 325
const newCircuitId = await this._connection!.invoke<string>(
'ResumeCircuit',
this._circuitId,
navigationManagerFunctions.getBaseURI(),
navigationManagerFunctions.getLocationHref(),
'[]',
''
persistedCircuitState?.components ?? '[]',
persistedCircuitState?.applicationState ?? '',
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending the state via hub invocation means we can only support up to 32KB of application state by default due to the SignalR message size limit, right? It'll probably be even less than that in reality because of the root component markers. Are we OK with that?

Comment on lines 288 to 294
public async resume(): Promise<boolean> {
if (!this._circuitId) {
throw new Error('Method not implemented.');
throw new Error('Circuit host not initialized.');
}

if (this._pausingPromise) {
await this._pausingPromise;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does resume() need to set this._paused back to false?

Comment on lines +44 to +45
isClientPause,
remotePause,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially thought the isClientPause and remotePause arguments would always have opposite values because I interpreted them as "client-initiated" vs. "server-initiated", but it's actually "are we gracefully pausing" and "if so, who initiated the pause", right? Maybe names like isGracefulPause and isRemoteInitiator might be clearer?

Comment on lines +141 to +150
connection.on('JS.SavePersistedState', (circuitId: string, components: string, applicationState: string) => {
if (!this._circuitId) {
throw new Error('Circuit host not initialized.');
}
if (circuitId !== this._circuitId) {
throw new Error(`Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.`);
}
this._persistedCircuitState = { components, applicationState };
return true;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like _persistedCircuitState is just stored in-memory. I've sometimes seen the Edge browser automatically refresh sleeping tabs, which would cause this state to be lost. Should we consider storing persisted state in e.g., local storage so that we handle cases like this?

@javiercn javiercn force-pushed the javiercn/persistent-component-state-ungraceful branch from 70da07d to 7820b1b Compare June 9, 2025 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants