Skip to content

Commit 7648018

Browse files
author
N. Taylor Mullen
committed
Improves further on Blazor reconnection experience.
- Expanded `ReconnectDisplay` to have a `rejected` method on it. This is the method that indicates we will never be able to reconnect to the server. By default we provide a nice little message letting users know that reconnection is no longer possible and that a refresh must take place. - Added a logger to the `DefaultReconnectionDisplay` since part of its job is handling `Retry` clicks which indirectly call `reconnect()`. Therefore, it needed the ability to log information to the console to inform users why certain reconnects were not possible. - Updated the `UserSpecifiedDisplay` to have a `refused` understanding. Added a new CSS class to represent the `refused` state as well. - Updated existing tests to abide by the new `ReconnectDisplay` structure - Added a new test to validate that the `refused``ReconnectDisplay` method results in proper behavior. #12442
1 parent 10452bf commit 7648018

9 files changed

+57
-21
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,8 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
3737
}
3838

3939
const reconnection = existingConnection || await initializeConnection(options, logger);
40-
if (reconnection.state !== signalR.HubConnectionState.Connected) {
41-
logger.log(LogLevel.Information, 'Reconnection attempt failed. Unable to connect to the server.');
42-
return false;
43-
}
44-
4540
if (!(await circuit.reconnect(reconnection))) {
46-
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit failed.');
41+
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
4742
return false;
4843
}
4944

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReconnectDisplay } from './ReconnectDisplay';
2+
import { Logger, LogLevel } from '../Logging/Logger';
23

34
export class DefaultReconnectDisplay implements ReconnectDisplay {
45
modal: HTMLDivElement;
@@ -11,7 +12,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
1112

1213
reloadParagraph: HTMLParagraphElement;
1314

14-
constructor(dialogId: string, private document: Document) {
15+
constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
1516
this.modal = this.document.createElement('div');
1617
this.modal.id = dialogId;
1718

@@ -38,8 +39,19 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
3839

3940
this.button.addEventListener('click', async () => {
4041
this.show();
41-
const successful = await window['Blazor'].reconnect();
42-
if (!successful) {
42+
43+
try {
44+
// reconnect will asynchronously return:
45+
// - true to mean success
46+
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
47+
// - exception to mean we didn't reach the server (this can be sync or async)
48+
const successful = await window['Blazor'].reconnect();
49+
if (!successful) {
50+
this.rejected();
51+
}
52+
} catch (err) {
53+
// We got an exception, server is currently unavailable
54+
this.logger.log(LogLevel.Error, err);
4355
this.failed();
4456
}
4557
});
@@ -66,4 +78,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
6678
this.reloadParagraph.style.display = 'none';
6779
this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
6880
}
81+
82+
rejected(): void {
83+
this.button.style.display = 'none';
84+
this.reloadParagraph.style.display = 'none';
85+
this.message.innerHTML = 'Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.';
86+
}
6987
}

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectionHandler.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
2121
const modal = document.getElementById(options.dialogId);
2222
this._reconnectionDisplay = modal
2323
? new UserSpecifiedDisplay(modal)
24-
: new DefaultReconnectDisplay(options.dialogId, document);
24+
: new DefaultReconnectDisplay(options.dialogId, document, this._logger);
2525
}
2626

2727
if (!this._currentReconnectionProcess) {
@@ -67,7 +67,8 @@ class ReconnectionProcess {
6767
const result = await this.reconnectCallback();
6868
if (!result) {
6969
// If the server responded and refused to reconnect, stop auto-retrying.
70-
break;
70+
this.reconnectDisplay.rejected();
71+
return;
7172
}
7273
return;
7374
} catch (err) {

src/Components/Web.JS/src/Platform/Circuits/ReconnectDisplay.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export interface ReconnectDisplay {
22
show(): void;
33
hide(): void;
44
failed(): void;
5+
rejected(): void;
56
}

src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
66

77
static readonly FailedClassName = 'components-reconnect-failed';
88

9+
static readonly RefusedClassName = 'components-reconnect-refused';
10+
911
constructor(private dialog: HTMLElement) {
1012
}
1113

@@ -23,8 +25,13 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
2325
this.removeClasses();
2426
this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
2527
}
28+
29+
rejected(): void {
30+
this.removeClasses();
31+
this.dialog.classList.add(UserSpecifiedDisplay.RefusedClassName);
32+
}
2633

2734
private removeClasses() {
28-
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName);
35+
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RefusedClassName);
2936
}
3037
}

src/Components/Web.JS/tests/DefaultReconnectDisplay.test.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
22
import {JSDOM} from 'jsdom';
3+
import { NullLogger} from '../src/Platform/Logging/Loggers';
34

45
describe('DefaultReconnectDisplay', () => {
56

67
it ('adds element to the body on show', () => {
78
const testDocument = new JSDOM().window.document;
8-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
9+
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
910

1011
display.show();
1112

@@ -20,7 +21,7 @@ describe('DefaultReconnectDisplay', () => {
2021

2122
it ('does not add element to the body multiple times', () => {
2223
const testDocument = new JSDOM().window.document;
23-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
24+
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
2425

2526
display.show();
2627
display.show();
@@ -30,7 +31,7 @@ describe('DefaultReconnectDisplay', () => {
3031

3132
it ('hides element', () => {
3233
const testDocument = new JSDOM().window.document;
33-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
34+
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
3435

3536
display.hide();
3637

@@ -39,7 +40,7 @@ describe('DefaultReconnectDisplay', () => {
3940

4041
it ('updates message on fail', () => {
4142
const testDocument = new JSDOM().window.document;
42-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
43+
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
4344

4445
display.show();
4546
display.failed();
@@ -49,4 +50,16 @@ describe('DefaultReconnectDisplay', () => {
4950
expect(display.button.style.display).toBe('block');
5051
});
5152

53+
it ('updates message on refused', () => {
54+
const testDocument = new JSDOM().window.document;
55+
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
56+
57+
display.show();
58+
display.rejected();
59+
60+
expect(display.modal.style.display).toBe('block');
61+
expect(display.message.innerHTML).toBe('Could not reconnect to the server. <a href=\"\">Reload</a> the page to restore functionality.');
62+
expect(display.button.style.display).toBe('none');
63+
});
64+
5265
});

src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ function createTestDisplay(): ReconnectDisplay {
9393
return {
9494
show: jest.fn(),
9595
hide: jest.fn(),
96-
failed: jest.fn()
96+
failed: jest.fn(),
97+
rejected: jest.fn()
9798
};
9899
}

0 commit comments

Comments
 (0)