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
This commit is contained in:
N. Taylor Mullen 2019-08-14 17:26:19 -07:00
parent b2a0b02e35
commit f890c9104c
9 changed files with 57 additions and 21 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,13 +37,8 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
}
const reconnection = existingConnection || await initializeConnection(options, logger);
if (reconnection.state !== signalR.HubConnectionState.Connected) {
logger.log(LogLevel.Information, 'Reconnection attempt failed. Unable to connect to the server.');
return false;
}
if (!(await circuit.reconnect(reconnection))) {
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit failed.');
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.');
return false;
}

View File

@ -1,4 +1,5 @@
import { ReconnectDisplay } from './ReconnectDisplay';
import { Logger, LogLevel } from '../Logging/Logger';
export class DefaultReconnectDisplay implements ReconnectDisplay {
modal: HTMLDivElement;
@ -11,7 +12,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
reloadParagraph: HTMLParagraphElement;
constructor(dialogId: string, private document: Document) {
constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
this.modal = this.document.createElement('div');
this.modal.id = dialogId;
@ -38,8 +39,19 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
this.button.addEventListener('click', async () => {
this.show();
const successful = await window['Blazor'].reconnect();
if (!successful) {
try {
// reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await window['Blazor'].reconnect();
if (!successful) {
this.rejected();
}
} catch (err) {
// We got an exception, server is currently unavailable
this.logger.log(LogLevel.Error, err);
this.failed();
}
});
@ -66,4 +78,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
this.reloadParagraph.style.display = 'none';
this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
}
rejected(): void {
this.button.style.display = 'none';
this.reloadParagraph.style.display = 'none';
this.message.innerHTML = 'Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.';
}
}

View File

@ -21,7 +21,7 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
const modal = document.getElementById(options.dialogId);
this._reconnectionDisplay = modal
? new UserSpecifiedDisplay(modal)
: new DefaultReconnectDisplay(options.dialogId, document);
: new DefaultReconnectDisplay(options.dialogId, document, this._logger);
}
if (!this._currentReconnectionProcess) {
@ -67,7 +67,8 @@ class ReconnectionProcess {
const result = await this.reconnectCallback();
if (!result) {
// If the server responded and refused to reconnect, stop auto-retrying.
break;
this.reconnectDisplay.rejected();
return;
}
return;
} catch (err) {

View File

@ -2,4 +2,5 @@ export interface ReconnectDisplay {
show(): void;
hide(): void;
failed(): void;
rejected(): void;
}

View File

@ -6,6 +6,8 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
static readonly FailedClassName = 'components-reconnect-failed';
static readonly RefusedClassName = 'components-reconnect-refused';
constructor(private dialog: HTMLElement) {
}
@ -23,8 +25,13 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
this.removeClasses();
this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
}
rejected(): void {
this.removeClasses();
this.dialog.classList.add(UserSpecifiedDisplay.RefusedClassName);
}
private removeClasses() {
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName);
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RefusedClassName);
}
}

View File

@ -1,11 +1,12 @@
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
import {JSDOM} from 'jsdom';
import { NullLogger} from '../src/Platform/Logging/Loggers';
describe('DefaultReconnectDisplay', () => {
it ('adds element to the body on show', () => {
const testDocument = new JSDOM().window.document;
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
display.show();
@ -20,7 +21,7 @@ describe('DefaultReconnectDisplay', () => {
it ('does not add element to the body multiple times', () => {
const testDocument = new JSDOM().window.document;
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
display.show();
display.show();
@ -30,7 +31,7 @@ describe('DefaultReconnectDisplay', () => {
it ('hides element', () => {
const testDocument = new JSDOM().window.document;
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
display.hide();
@ -39,7 +40,7 @@ describe('DefaultReconnectDisplay', () => {
it ('updates message on fail', () => {
const testDocument = new JSDOM().window.document;
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
display.show();
display.failed();
@ -49,4 +50,16 @@ describe('DefaultReconnectDisplay', () => {
expect(display.button.style.display).toBe('block');
});
it ('updates message on refused', () => {
const testDocument = new JSDOM().window.document;
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
display.show();
display.rejected();
expect(display.modal.style.display).toBe('block');
expect(display.message.innerHTML).toBe('Could not reconnect to the server. <a href=\"\">Reload</a> the page to restore functionality.');
expect(display.button.style.display).toBe('none');
});
});

View File

@ -93,6 +93,7 @@ function createTestDisplay(): ReconnectDisplay {
return {
show: jest.fn(),
hide: jest.fn(),
failed: jest.fn()
failed: jest.fn(),
rejected: jest.fn()
};
}