Add enhancement to default behavior of client reconnection (#24992)
These changes are to improve the default reconnection behavior of the client - [x] Match the reconnection time with server side - [x] Add indicator to know at which reconnection attempt we currently are - [x] [Additional] Add a loader symbol - [x] Add client side test Addresses #18745
This commit is contained in:
parent
c181218a4e
commit
601fc20ece
File diff suppressed because one or more lines are too long
|
|
@ -33,8 +33,8 @@ const defaultOptions: CircuitStartOptions = {
|
|||
configureSignalR: (_) => { },
|
||||
logLevel: LogLevel.Warning,
|
||||
reconnectionOptions: {
|
||||
maxRetries: 5,
|
||||
retryIntervalMilliseconds: 3000,
|
||||
maxRetries: 8,
|
||||
retryIntervalMilliseconds: 20000,
|
||||
dialogId: 'components-reconnect-modal',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
|
||||
reloadParagraph: HTMLParagraphElement;
|
||||
|
||||
constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
|
||||
loader: HTMLDivElement;
|
||||
|
||||
constructor(dialogId: string, private readonly maxRetries: number, private readonly document: Document, private readonly logger: Logger) {
|
||||
this.modal = this.document.createElement('div');
|
||||
this.modal.id = dialogId;
|
||||
this.maxRetries = maxRetries;
|
||||
|
||||
const modalStyles = [
|
||||
'position: fixed',
|
||||
|
|
@ -37,6 +40,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
this.message = this.modal.querySelector('h5')!;
|
||||
this.button = this.modal.querySelector('button')!;
|
||||
this.reloadParagraph = this.modal.querySelector('p')!;
|
||||
this.loader = this.getLoader();
|
||||
|
||||
this.message.after(this.loader);
|
||||
|
||||
this.button.addEventListener('click', async () => {
|
||||
this.show();
|
||||
|
|
@ -65,6 +71,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
this.document.body.appendChild(this.modal);
|
||||
}
|
||||
this.modal.style.display = 'block';
|
||||
this.loader.style.display = 'inline-block';
|
||||
this.button.style.display = 'none';
|
||||
this.reloadParagraph.style.display = 'none';
|
||||
this.message.textContent = 'Attempting to reconnect to the server...';
|
||||
|
|
@ -78,6 +85,10 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
update(currentAttempt: number): void {
|
||||
this.message.textContent = `Attempting to reconnect to the server: ${currentAttempt} of ${this.maxRetries}`;
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.modal.style.display = 'none';
|
||||
}
|
||||
|
|
@ -85,6 +96,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
failed(): void {
|
||||
this.button.style.display = 'block';
|
||||
this.reloadParagraph.style.display = 'none';
|
||||
this.loader.style.display = 'none';
|
||||
this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
|
||||
this.message.querySelector('a')!.addEventListener('click', () => location.reload());
|
||||
}
|
||||
|
|
@ -92,7 +104,32 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
rejected(): void {
|
||||
this.button.style.display = 'none';
|
||||
this.reloadParagraph.style.display = 'none';
|
||||
this.loader.style.display = 'none';
|
||||
this.message.innerHTML = 'Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.';
|
||||
this.message.querySelector('a')!.addEventListener('click', () => location.reload());
|
||||
}
|
||||
|
||||
private getLoader(): HTMLDivElement {
|
||||
const loader = this.document.createElement('div');
|
||||
|
||||
const loaderStyles = [
|
||||
'border: 0.3em solid #f3f3f3',
|
||||
'border-top: 0.3em solid #3498db',
|
||||
'border-radius: 50%',
|
||||
'width: 2em',
|
||||
'height: 2em',
|
||||
'display: inline-block'
|
||||
];
|
||||
|
||||
loader.style.cssText = loaderStyles.join(';');
|
||||
loader.animate([
|
||||
{ transform: 'rotate(0deg)' },
|
||||
{ transform: 'rotate(360deg)' }
|
||||
], {
|
||||
duration: 2000,
|
||||
iterations: Infinity
|
||||
});
|
||||
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
|
|||
if (!this._reconnectionDisplay) {
|
||||
const modal = document.getElementById(options.dialogId);
|
||||
this._reconnectionDisplay = modal
|
||||
? new UserSpecifiedDisplay(modal)
|
||||
: new DefaultReconnectDisplay(options.dialogId, document, this._logger);
|
||||
? new UserSpecifiedDisplay(modal, options.maxRetries, document)
|
||||
: new DefaultReconnectDisplay(options.dialogId, options.maxRetries, document, this._logger);
|
||||
}
|
||||
|
||||
if (!this._currentReconnectionProcess) {
|
||||
|
|
@ -38,6 +38,8 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
|
|||
};
|
||||
|
||||
class ReconnectionProcess {
|
||||
static readonly MaximumFirstRetryInterval = 3000;
|
||||
|
||||
readonly reconnectDisplay: ReconnectDisplay;
|
||||
isDisposed = false;
|
||||
|
||||
|
|
@ -54,7 +56,13 @@ class ReconnectionProcess {
|
|||
|
||||
async attemptPeriodicReconnection(options: ReconnectionOptions) {
|
||||
for (let i = 0; i < options.maxRetries; i++) {
|
||||
await this.delay(options.retryIntervalMilliseconds);
|
||||
this.reconnectDisplay.update(i + 1);
|
||||
|
||||
const delayDuration = i == 0 && options.retryIntervalMilliseconds > ReconnectionProcess.MaximumFirstRetryInterval
|
||||
? ReconnectionProcess.MaximumFirstRetryInterval
|
||||
: options.retryIntervalMilliseconds;
|
||||
await this.delay(delayDuration);
|
||||
|
||||
if (this.isDisposed) {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export interface ReconnectDisplay {
|
||||
show(): void;
|
||||
update(currentAttempt: number): void;
|
||||
hide(): void;
|
||||
failed(): void;
|
||||
rejected(): void;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,18 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
|
|||
|
||||
static readonly RejectedClassName = 'components-reconnect-rejected';
|
||||
|
||||
constructor(private dialog: HTMLElement) {
|
||||
static readonly MaxRetriesId = 'components-reconnect-max-retries';
|
||||
|
||||
static readonly CurrentAttemptId = 'components-reconnect-current-attempt';
|
||||
|
||||
constructor(private dialog: HTMLElement, private readonly maxRetries: number, private readonly document: Document) {
|
||||
this.document = document;
|
||||
|
||||
const maxRetriesElement = this.document.getElementById(UserSpecifiedDisplay.MaxRetriesId);
|
||||
|
||||
if (maxRetriesElement) {
|
||||
maxRetriesElement.innerText = this.maxRetries.toString();
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
|
|
@ -16,6 +27,14 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
|
|||
this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName);
|
||||
}
|
||||
|
||||
update(currentAttempt: number): void {
|
||||
const currentAttemptElement = this.document.getElementById(UserSpecifiedDisplay.CurrentAttemptId);
|
||||
|
||||
if (currentAttemptElement) {
|
||||
currentAttemptElement.innerText = currentAttempt.toString();
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.removeClasses();
|
||||
this.dialog.classList.add(UserSpecifiedDisplay.HideClassName);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,18 @@ import { JSDOM } from 'jsdom';
|
|||
import { NullLogger } from '../src/Platform/Logging/Loggers';
|
||||
|
||||
describe('DefaultReconnectDisplay', () => {
|
||||
let testDocument: Document;
|
||||
|
||||
beforeEach(() => {
|
||||
const window = new JSDOM().window;
|
||||
|
||||
//JSDOM does not support animate function so we need to mock it
|
||||
window.HTMLDivElement.prototype.animate = jest.fn();
|
||||
testDocument = window.document;
|
||||
})
|
||||
|
||||
it ('adds element to the body on show', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
|
||||
|
||||
display.show();
|
||||
|
||||
|
|
@ -16,6 +24,7 @@ describe('DefaultReconnectDisplay', () => {
|
|||
expect(element!.style.display).toBe('block');
|
||||
expect(element!.style.visibility).toBe('hidden');
|
||||
|
||||
expect(display.loader.style.display).toBe('inline-block');
|
||||
expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
|
||||
expect(display.button.style.display).toBe('none');
|
||||
|
||||
|
|
@ -27,8 +36,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, NullLogger.instance);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
|
||||
|
||||
display.show();
|
||||
display.show();
|
||||
|
|
@ -37,8 +45,7 @@ describe('DefaultReconnectDisplay', () => {
|
|||
});
|
||||
|
||||
it ('hides element', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
|
||||
|
||||
display.hide();
|
||||
|
||||
|
|
@ -46,8 +53,7 @@ describe('DefaultReconnectDisplay', () => {
|
|||
});
|
||||
|
||||
it ('updates message on fail', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
|
||||
|
||||
display.show();
|
||||
display.failed();
|
||||
|
|
@ -55,11 +61,11 @@ describe('DefaultReconnectDisplay', () => {
|
|||
expect(display.modal.style.display).toBe('block');
|
||||
expect(display.message.innerHTML).toBe('Reconnection failed. Try <a href=\"\">reloading</a> the page if you\'re unable to reconnect.');
|
||||
expect(display.button.style.display).toBe('block');
|
||||
expect(display.loader.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it ('updates message on refused', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
|
||||
|
||||
display.show();
|
||||
display.rejected();
|
||||
|
|
@ -67,6 +73,18 @@ describe('DefaultReconnectDisplay', () => {
|
|||
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');
|
||||
expect(display.loader.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('update message with current attempt', () => {
|
||||
const maxRetires = 6;
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', maxRetires, testDocument, NullLogger.instance);
|
||||
|
||||
display.show();
|
||||
|
||||
for (let index = 0; index < maxRetires; index++) {
|
||||
display.update(index);
|
||||
expect(display.message.innerHTML).toBe(`Attempting to reconnect to the server: ${index++} of ${maxRetires}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,6 +75,23 @@ describe('DefaultReconnectionHandler', () => {
|
|||
expect(testDisplay.failed).toHaveBeenCalled();
|
||||
expect(reconnect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('invokes update on each attempt', async () => {
|
||||
const testDisplay = createTestDisplay();
|
||||
const reconnect = jest.fn().mockRejectedValue(null);
|
||||
const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
|
||||
const maxRetries = 6;
|
||||
|
||||
handler.onConnectionDown({
|
||||
maxRetries: maxRetries,
|
||||
retryIntervalMilliseconds: 5,
|
||||
dialogId: 'ignored'
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
expect(testDisplay.update).toHaveBeenCalledTimes(maxRetries);
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
function attachUserSpecifiedUI(options: ReconnectionOptions): Element {
|
||||
|
|
@ -92,6 +109,7 @@ function delay(durationMilliseconds: number) {
|
|||
function createTestDisplay(): ReconnectDisplay {
|
||||
return {
|
||||
show: jest.fn(),
|
||||
update: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
failed: jest.fn(),
|
||||
rejected: jest.fn()
|
||||
|
|
|
|||
Loading…
Reference in New Issue