Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {

rejoiningAnimation: HTMLDivElement;

reloadButton: HTMLButtonElement;
retryButton: HTMLButtonElement;

resumeButton: HTMLButtonElement;

status: HTMLParagraphElement;

reconnect = true;
operation: ReconnectDisplayUpdateOptions['type'] = 'reconnect';

remote = false;

graceful = false;

retryWhenDocumentBecomesVisible: () => void;

constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
Expand Down Expand Up @@ -65,10 +67,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
this.status = document.createElement('p');
this.status.innerHTML = '';

this.reloadButton = document.createElement('button');
this.reloadButton.style.display = 'none';
this.reloadButton.innerHTML = 'Retry';
this.reloadButton.addEventListener('click', this.retry.bind(this));
this.retryButton = document.createElement('button');
this.retryButton.style.display = 'none';
this.retryButton.innerHTML = 'Retry';
this.retryButton.addEventListener('click', this.retry.bind(this));

this.resumeButton = document.createElement('button');
this.resumeButton.style.display = 'none';
Expand All @@ -77,7 +79,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {

this.dialog.appendChild(this.rejoiningAnimation);
this.dialog.appendChild(this.status);
this.dialog.appendChild(this.reloadButton);
this.dialog.appendChild(this.retryButton);
this.dialog.appendChild(this.resumeButton);

this.overlay.appendChild(this.dialog);
Expand All @@ -94,30 +96,33 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
this.document.body.appendChild(this.host);
}

this.reconnect = options?.type === 'reconnect';
this.operation = options?.type ?? 'reconnect';
this.graceful = (options?.type === 'pause') ? options.graceful : false;

this.reloadButton.style.display = 'none';
this.retryButton.style.display = 'none';
this.rejoiningAnimation.style.display = 'block';
this.status.innerHTML = 'Rejoining the server...';
this.host.style.display = 'block';
this.overlay.classList.add(DefaultReconnectDisplay.ReconnectVisibleClassName);
}

update(options: ReconnectDisplayUpdateOptions): void {
this.reconnect = options.type === 'reconnect';
if (this.reconnect) {
this.operation = options.type;
this.graceful = (options.type === 'pause') ? options.graceful : false;

if (this.operation === 'pause') {
this.retryButton.style.display = 'none';
this.rejoiningAnimation.style.display = 'none';
this.status.innerHTML = 'The session has been paused by the server.';
this.resumeButton.style.display = 'block';
} else {
const { currentAttempt, secondsToNextAttempt } = options as ReconnectOptions;
if (currentAttempt === 1 || secondsToNextAttempt === 0) {
this.status.innerHTML = 'Rejoining the server...';
} else {
const unitText = secondsToNextAttempt === 1 ? 'second' : 'seconds';
this.status.innerHTML = `Rejoin failed... trying again in ${secondsToNextAttempt} ${unitText}`;
}
} else {
this.reloadButton.style.display = 'none';
this.rejoiningAnimation.style.display = 'none';
this.status.innerHTML = 'The session has been paused by the server.';
this.resumeButton.style.display = 'block';
}
}

Expand All @@ -128,14 +133,21 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {

failed(): void {
this.rejoiningAnimation.style.display = 'none';
if (this.reconnect) {
this.reloadButton.style.display = 'block';
if (this.operation === 'pause') {
if (this.graceful) {
// Circuit failed to resume after a graceful (client-requested) pause.
// We show a retry UI to allow the user to try to continue without losing the state.
this.resumeButton.style.display = 'block';
this.status.innerHTML = 'Failed to resume.<br />Please retry or reload the page.';
} else {
// Circuit failed to resume after an ungraceful pause (e.g., server restart).
// We treat this as non-recoverable rejection.
this.rejected();
}
} else {
this.retryButton.style.display = 'block';
this.status.innerHTML = 'Failed to rejoin.<br />Please retry or reload the page.';
this.document.addEventListener('visibilitychange', this.retryWhenDocumentBecomesVisible);
} else {
this.status.innerHTML = 'Failed to resume the session.<br />Please reload the page.';
this.resumeButton.style.display = 'none';
this.reloadButton.style.display = 'none';
}
}

Expand All @@ -157,10 +169,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
const successful = await Blazor.reconnect!();
if (!successful) {
// Try to resume the circuit if the reconnect failed
this.update({ type: 'pause', remote: this.remote });
this.update({ type: 'pause', remote: this.remote, graceful: this.graceful });
const resumeSuccessful = await Blazor.resumeCircuit!();
if (!resumeSuccessful) {
this.rejected();
this.failed();
}
}
} catch (err: unknown) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class ReconnectionProcess {
const displayOptions: ReconnectDisplayUpdateOptions = {
type: isGracefulPause ? 'pause' : 'reconnect',
remote: this.isRemote,
graceful: !!isGracefulPause,
currentAttempt: 0,
secondsToNextAttempt: 0,
};
Expand All @@ -85,6 +86,7 @@ class ReconnectionProcess {
this.reconnectDisplay.update({
type: 'pause',
remote: this.isRemote,
graceful: true,
});
}
}
Expand Down Expand Up @@ -126,7 +128,7 @@ class ReconnectionProcess {
if (!result) {
// Try to resume the circuit if the reconnect failed
// If the server responded and refused to reconnect, stop auto-retrying.
this.reconnectDisplay.update({ type: 'pause', remote: true });
this.reconnectDisplay.update({ type: 'pause', remote: true, graceful: false });
const resumeResult = await this.resumeCallback();
if (resumeResult) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export type ReconnectDisplayUpdateOptions = ReconnectOptions | PauseOptions;

export type PauseOptions = {
type: 'pause',
remote: boolean
remote: boolean,
graceful: boolean
Copy link
Member

Choose a reason for hiding this comment

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

Remote and graceful represent the same concept. remote resumes are always ungraceful and vice versa.

Copy link
Member Author

Choose a reason for hiding this comment

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

I see, I thought those were orthogonal properties. Then it simplifies the change. I will update the PR.

};

export type ReconnectOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface ReconnectStateChangedEvent {
currentAttempt?: number;
secondsToNextAttempt?: number;
remote?: boolean;
graceful?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
}
if (options.type === 'pause') {
const remote = options.remote;
const graceful = options.graceful;
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.RetryingClassName);
this.dialog.classList.add(UserSpecifiedDisplay.PausedClassName);
this.dispatchReconnectStateChangedEvent({ state: 'paused', remote: remote });
this.dispatchReconnectStateChangedEvent({ state: 'paused', remote: remote, graceful: graceful });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<button id="components-resume-button" class="components-pause-visible">
Resume
</button>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please reload the page.
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function handleReconnectStateChanged(event) {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
} else if (event.detail.state === "rejected" || (event.detail.state === "resume-failed" && !!event.detail.graceful)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
} else if (event.detail.state === "rejected" || (event.detail.state === "resume-failed" && !!event.detail.graceful)) {
} else if (event.detail.state === "rejected" || (event.detail.state === "resume-failed" && event.detail.remote))

Copy link

Choose a reason for hiding this comment

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

@javiercn your earlier suggestion of just
} else if (event.detail.state === "rejected" || event.detail.state === "resume-failed") {
provides a better empty new circuit experience.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, not sure what you mean. We don't want to reload the page unconditionally when the circuit fails to resume. We only want to do that when it happens as a result of the app disconnecting, but not when the app explicitly pauses the circuit.

For ungraceful disconnections the client is not in control, but for pausing and then resuming, the client is. We don't want to unconditionally reload the page by default because that precludes users from saving any data that they had on the page.

Example, you are filling in a long form field, you switch to a different tab. Your JS code "pauses" the circuit when the visibility changes. When the user comes back to the tab and you try to "resume" the circuit, if for some reason the resume operation fails, we don't want to result in a refresh.

In the case of reconnection, the client app is not in control, but in case of reconnection, the client triggered the "pause" operation in the first place and should have some code to handle things when the "resume" goes wrong.

I would argue that automatically reloading is also a bad policy to have for reconnection, as you run into a similar problem, but I think that ship has sailed.

We want to restore the behavior for failed reconnections + failed resumes by default, but we don't want to automatically reload the page in case of failed resumes when the app triggered the pause.

location.reload();
}
}
Expand Down
Loading