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:
parent
b2a0b02e35
commit
f890c9104c
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -37,13 +37,8 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnection = existingConnection || await initializeConnection(options, logger);
|
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))) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||||
|
import { Logger, LogLevel } from '../Logging/Logger';
|
||||||
|
|
||||||
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
modal: HTMLDivElement;
|
modal: HTMLDivElement;
|
||||||
|
|
@ -11,7 +12,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
|
|
||||||
reloadParagraph: HTMLParagraphElement;
|
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 = this.document.createElement('div');
|
||||||
this.modal.id = dialogId;
|
this.modal.id = dialogId;
|
||||||
|
|
||||||
|
|
@ -38,8 +39,19 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
|
|
||||||
this.button.addEventListener('click', async () => {
|
this.button.addEventListener('click', async () => {
|
||||||
this.show();
|
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();
|
this.failed();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -66,4 +78,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
this.reloadParagraph.style.display = 'none';
|
this.reloadParagraph.style.display = 'none';
|
||||||
this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
|
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.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
|
||||||
const modal = document.getElementById(options.dialogId);
|
const modal = document.getElementById(options.dialogId);
|
||||||
this._reconnectionDisplay = modal
|
this._reconnectionDisplay = modal
|
||||||
? new UserSpecifiedDisplay(modal)
|
? new UserSpecifiedDisplay(modal)
|
||||||
: new DefaultReconnectDisplay(options.dialogId, document);
|
: new DefaultReconnectDisplay(options.dialogId, document, this._logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._currentReconnectionProcess) {
|
if (!this._currentReconnectionProcess) {
|
||||||
|
|
@ -67,7 +67,8 @@ class ReconnectionProcess {
|
||||||
const result = await this.reconnectCallback();
|
const result = await this.reconnectCallback();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// If the server responded and refused to reconnect, stop auto-retrying.
|
// If the server responded and refused to reconnect, stop auto-retrying.
|
||||||
break;
|
this.reconnectDisplay.rejected();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,5 @@ export interface ReconnectDisplay {
|
||||||
show(): void;
|
show(): void;
|
||||||
hide(): void;
|
hide(): void;
|
||||||
failed(): void;
|
failed(): void;
|
||||||
|
rejected(): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
|
||||||
|
|
||||||
static readonly FailedClassName = 'components-reconnect-failed';
|
static readonly FailedClassName = 'components-reconnect-failed';
|
||||||
|
|
||||||
|
static readonly RefusedClassName = 'components-reconnect-refused';
|
||||||
|
|
||||||
constructor(private dialog: HTMLElement) {
|
constructor(private dialog: HTMLElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,8 +25,13 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
|
||||||
this.removeClasses();
|
this.removeClasses();
|
||||||
this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
|
this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rejected(): void {
|
||||||
|
this.removeClasses();
|
||||||
|
this.dialog.classList.add(UserSpecifiedDisplay.RefusedClassName);
|
||||||
|
}
|
||||||
|
|
||||||
private removeClasses() {
|
private removeClasses() {
|
||||||
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName);
|
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RefusedClassName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
|
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
|
||||||
import {JSDOM} from 'jsdom';
|
import {JSDOM} from 'jsdom';
|
||||||
|
import { NullLogger} from '../src/Platform/Logging/Loggers';
|
||||||
|
|
||||||
describe('DefaultReconnectDisplay', () => {
|
describe('DefaultReconnectDisplay', () => {
|
||||||
|
|
||||||
it ('adds element to the body on show', () => {
|
it ('adds element to the body on show', () => {
|
||||||
const testDocument = new JSDOM().window.document;
|
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();
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ describe('DefaultReconnectDisplay', () => {
|
||||||
|
|
||||||
it ('does not add element to the body multiple times', () => {
|
it ('does not add element to the body multiple times', () => {
|
||||||
const testDocument = new JSDOM().window.document;
|
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();
|
||||||
display.show();
|
display.show();
|
||||||
|
|
@ -30,7 +31,7 @@ describe('DefaultReconnectDisplay', () => {
|
||||||
|
|
||||||
it ('hides element', () => {
|
it ('hides element', () => {
|
||||||
const testDocument = new JSDOM().window.document;
|
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();
|
display.hide();
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ describe('DefaultReconnectDisplay', () => {
|
||||||
|
|
||||||
it ('updates message on fail', () => {
|
it ('updates message on fail', () => {
|
||||||
const testDocument = new JSDOM().window.document;
|
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();
|
||||||
display.failed();
|
display.failed();
|
||||||
|
|
@ -49,4 +50,16 @@ describe('DefaultReconnectDisplay', () => {
|
||||||
expect(display.button.style.display).toBe('block');
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ function createTestDisplay(): ReconnectDisplay {
|
||||||
return {
|
return {
|
||||||
show: jest.fn(),
|
show: jest.fn(),
|
||||||
hide: jest.fn(),
|
hide: jest.fn(),
|
||||||
failed: jest.fn()
|
failed: jest.fn(),
|
||||||
|
rejected: jest.fn()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue