* Some initial tidying on Boot.Server.ts, though can't make much difference until stateful prerendering is removed * In Web.JS, rename ILogger to Logger to match TypeScript conventions * Move reconnection options into BlazorOptions * In Web.JS, eliminate collection of CircuitHandlers and just have one ReconnectionHandler * Expose Blazor.defaultReconnectionHandler * Update binaries
This commit is contained in:
parent
e808a4f2ed
commit
e451597a0a
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -3,52 +3,33 @@ import './GlobalExports';
|
|||
import * as signalR from '@aspnet/signalr';
|
||||
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
|
||||
import { shouldAutoStart } from './BootCommon';
|
||||
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
|
||||
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
|
||||
import RenderQueue from './Platform/Circuits/RenderQueue';
|
||||
import { RenderQueue } from './Platform/Circuits/RenderQueue';
|
||||
import { ConsoleLogger } from './Platform/Logging/Loggers';
|
||||
import { LogLevel, ILogger } from './Platform/Logging/ILogger';
|
||||
import { LogLevel, Logger } from './Platform/Logging/Logger';
|
||||
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
|
||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||
|
||||
|
||||
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
|
||||
interface BlazorOptions {
|
||||
configureSignalR: SignalRBuilder;
|
||||
logLevel: LogLevel;
|
||||
}
|
||||
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
|
||||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||
|
||||
let renderingFailed = false;
|
||||
let started = false;
|
||||
|
||||
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||
|
||||
if (started) {
|
||||
throw new Error('Blazor has already started.');
|
||||
}
|
||||
started = true;
|
||||
|
||||
const defaultOptions: BlazorOptions = {
|
||||
configureSignalR: (_) => { },
|
||||
logLevel: LogLevel.Warning,
|
||||
};
|
||||
|
||||
const options: BlazorOptions = { ...defaultOptions, ...userOptions };
|
||||
|
||||
// For development.
|
||||
// Simply put a break point here and modify the log level during
|
||||
// development to get traces.
|
||||
// In the future we will allow for users to configure this.
|
||||
// Establish options to be used
|
||||
const options = resolveOptions(userOptions);
|
||||
const logger = new ConsoleLogger(options.logLevel);
|
||||
|
||||
window['Blazor'].defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
|
||||
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
|
||||
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
|
||||
|
||||
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
|
||||
window['Blazor'].circuitHandlers = circuitHandlers;
|
||||
|
||||
// pass options.configureSignalR to configure the signalR.HubConnectionBuilder
|
||||
const initialConnection = await initializeConnection(options, circuitHandlers, logger);
|
||||
|
||||
// Initialize statefully prerendered circuits and their components
|
||||
// Note: This will all be removed soon
|
||||
const initialConnection = await initializeConnection(options, logger);
|
||||
const circuits = discoverPrerenderedCircuits(document);
|
||||
for (let i = 0; i < circuits.length; i++) {
|
||||
const circuit = circuits[i];
|
||||
|
|
@ -59,7 +40,6 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
|||
}
|
||||
|
||||
const circuit = await startCircuit(initialConnection);
|
||||
|
||||
if (!circuit) {
|
||||
logger.log(LogLevel.Information, 'No preregistered components to render.');
|
||||
}
|
||||
|
|
@ -69,14 +49,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
|||
// We can't reconnect after a failure, so exit early.
|
||||
return false;
|
||||
}
|
||||
const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, logger);
|
||||
const reconnection = existingConnection || await initializeConnection(options, logger);
|
||||
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
|
||||
|
||||
if (reconnectionFailed(results)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
|
||||
options.reconnectionHandler!.onConnectionUp();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -97,8 +78,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
|
||||
|
||||
async function initializeConnection(options: BlazorOptions, logger: Logger): Promise<signalR.HubConnection> {
|
||||
const hubProtocol = new MessagePackHubProtocol();
|
||||
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
|
||||
|
||||
|
|
@ -124,7 +104,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
|
|||
queue.processBatch(batchId, batchData, connection);
|
||||
});
|
||||
|
||||
connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
|
||||
connection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
|
||||
connection.on('JS.Error', error => unhandledError(connection, error, logger));
|
||||
|
||||
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
|
||||
|
|
@ -147,7 +127,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
|
|||
return connection;
|
||||
}
|
||||
|
||||
function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
|
||||
function unhandledError(connection: signalR.HubConnection, err: Error, logger: Logger): void {
|
||||
logger.log(LogLevel.Error, err);
|
||||
|
||||
// Disconnect on errors.
|
||||
|
|
@ -160,6 +140,7 @@ function unhandledError(connection: signalR.HubConnection, err: Error, logger: I
|
|||
}
|
||||
|
||||
window['Blazor'].start = boot;
|
||||
|
||||
if (shouldAutoStart()) {
|
||||
boot();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ interface BootJsonData {
|
|||
|
||||
// Tells you if the script was added without <script src="..." autostart="false"></script>
|
||||
export function shouldAutoStart() {
|
||||
return document &&
|
||||
return !!(document &&
|
||||
document.currentScript &&
|
||||
document.currentScript.getAttribute('autostart') !== 'false';
|
||||
}
|
||||
document.currentScript.getAttribute('autostart') !== 'false');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { CircuitHandler } from './CircuitHandler';
|
||||
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
|
||||
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
|
||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { ILogger, LogLevel } from '../Logging/ILogger';
|
||||
export class AutoReconnectCircuitHandler implements CircuitHandler {
|
||||
public static readonly MaxRetries = 5;
|
||||
|
||||
public static readonly RetryInterval = 3000;
|
||||
|
||||
public static readonly DialogId = 'components-reconnect-modal';
|
||||
|
||||
public reconnectDisplay: ReconnectDisplay;
|
||||
|
||||
public logger: ILogger;
|
||||
|
||||
public constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.reconnectDisplay = new DefaultReconnectDisplay(document);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
|
||||
if (modal) {
|
||||
this.reconnectDisplay = new UserSpecifiedDisplay(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onConnectionUp(): void {
|
||||
this.reconnectDisplay.hide();
|
||||
}
|
||||
|
||||
public delay(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
|
||||
}
|
||||
|
||||
public async onConnectionDown(): Promise<void> {
|
||||
this.reconnectDisplay.show();
|
||||
|
||||
for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
|
||||
await this.delay();
|
||||
try {
|
||||
const result = await window['Blazor'].reconnect();
|
||||
if (!result) {
|
||||
// If the server responded and refused to reconnect, stop auto-retrying.
|
||||
break;
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
this.logger.log(LogLevel.Error, err);
|
||||
}
|
||||
}
|
||||
|
||||
this.reconnectDisplay.failed();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { LogLevel } from '../Logging/Logger';
|
||||
|
||||
export interface BlazorOptions {
|
||||
configureSignalR: (builder: signalR.HubConnectionBuilder) => void;
|
||||
logLevel: LogLevel;
|
||||
reconnectionOptions: ReconnectionOptions;
|
||||
reconnectionHandler?: ReconnectionHandler;
|
||||
}
|
||||
|
||||
export function resolveOptions(userOptions?: Partial<BlazorOptions>): BlazorOptions {
|
||||
const result = { ...defaultOptions, ...userOptions };
|
||||
|
||||
// The spread operator can't be used for a deep merge, so do the same for subproperties
|
||||
if (userOptions && userOptions.reconnectionOptions) {
|
||||
result.reconnectionOptions = { ...defaultOptions.reconnectionOptions, ...userOptions.reconnectionOptions };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface ReconnectionOptions {
|
||||
maxRetries: number;
|
||||
retryIntervalMilliseconds: number;
|
||||
dialogId: string;
|
||||
}
|
||||
|
||||
export interface ReconnectionHandler {
|
||||
onConnectionDown(options: ReconnectionOptions, error?: Error): void;
|
||||
onConnectionUp(): void;
|
||||
}
|
||||
|
||||
const defaultOptions: BlazorOptions = {
|
||||
configureSignalR: (_) => { },
|
||||
logLevel: LogLevel.Warning,
|
||||
reconnectionOptions: {
|
||||
maxRetries: 5,
|
||||
retryIntervalMilliseconds: 3000,
|
||||
dialogId: 'components-reconnect-modal',
|
||||
},
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export interface CircuitHandler {
|
||||
/** Invoked when a server connection is established or re-established after a connection failure.
|
||||
*/
|
||||
onConnectionUp?(): void;
|
||||
|
||||
/** Invoked when a server connection is dropped.
|
||||
* @param {Error} error Optionally argument containing the error that caused the connection to close (if any).
|
||||
*/
|
||||
onConnectionDown?(error?: Error): void;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
|
||||
|
||||
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||
modal: HTMLDivElement;
|
||||
|
||||
|
|
@ -9,9 +9,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
|
||||
addedToDom: boolean = false;
|
||||
|
||||
constructor(private document: Document) {
|
||||
constructor(dialogId: string, private document: Document) {
|
||||
this.modal = this.document.createElement('div');
|
||||
this.modal.id = AutoReconnectCircuitHandler.DialogId;
|
||||
this.modal.id = dialogId;
|
||||
|
||||
const modalStyles = [
|
||||
'position: fixed',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { ReconnectionHandler, ReconnectionOptions } from './BlazorOptions';
|
||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
|
||||
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
|
||||
import { Logger, LogLevel } from '../Logging/Logger';
|
||||
|
||||
export class DefaultReconnectionHandler implements ReconnectionHandler {
|
||||
private readonly _logger: Logger;
|
||||
private readonly _overrideDisplay?: ReconnectDisplay;
|
||||
private readonly _reconnectCallback: () => Promise<boolean>;
|
||||
private _currentReconnectionProcess: ReconnectionProcess | null = null;
|
||||
|
||||
constructor(logger: Logger, overrideDisplay?: ReconnectDisplay, reconnectCallback?: () => Promise<boolean>) {
|
||||
this._logger = logger;
|
||||
this._overrideDisplay = overrideDisplay;
|
||||
this._reconnectCallback = reconnectCallback || (() => window['Blazor'].reconnect());
|
||||
}
|
||||
|
||||
onConnectionDown (options: ReconnectionOptions, error?: Error) {
|
||||
if (!this._currentReconnectionProcess) {
|
||||
this._currentReconnectionProcess = new ReconnectionProcess(options, this._logger, this._reconnectCallback, this._overrideDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
onConnectionUp() {
|
||||
if (this._currentReconnectionProcess) {
|
||||
this._currentReconnectionProcess.dispose();
|
||||
this._currentReconnectionProcess = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ReconnectionProcess {
|
||||
readonly reconnectDisplay: ReconnectDisplay;
|
||||
isDisposed = false;
|
||||
|
||||
constructor(options: ReconnectionOptions, private logger: Logger, private reconnectCallback: () => Promise<boolean>, display?: ReconnectDisplay) {
|
||||
const modal = document.getElementById(options.dialogId);
|
||||
this.reconnectDisplay = display || (modal
|
||||
? new UserSpecifiedDisplay(modal)
|
||||
: new DefaultReconnectDisplay(options.dialogId, document));
|
||||
|
||||
this.reconnectDisplay.show();
|
||||
this.attemptPeriodicReconnection(options);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.isDisposed = true;
|
||||
this.reconnectDisplay.hide();
|
||||
}
|
||||
|
||||
async attemptPeriodicReconnection(options: ReconnectionOptions) {
|
||||
for (let i = 0; i < options.maxRetries; i++) {
|
||||
await this.delay(options.retryIntervalMilliseconds);
|
||||
if (this.isDisposed) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// reconnectCallback 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 result = await this.reconnectCallback();
|
||||
if (!result) {
|
||||
// If the server responded and refused to reconnect, stop auto-retrying.
|
||||
break;
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
// We got an exception so will try again momentarily
|
||||
this.logger.log(LogLevel.Error, err);
|
||||
}
|
||||
}
|
||||
|
||||
this.reconnectDisplay.failed();
|
||||
}
|
||||
|
||||
delay(durationMilliseconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import { renderBatch } from '../../Rendering/Renderer';
|
||||
import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
|
||||
import { ILogger, LogLevel } from '../Logging/ILogger';
|
||||
import { Logger, LogLevel } from '../Logging/Logger';
|
||||
import { HubConnection } from '@aspnet/signalr';
|
||||
|
||||
export default class RenderQueue {
|
||||
export class RenderQueue {
|
||||
private static renderQueues = new Map<number, RenderQueue>();
|
||||
|
||||
private nextBatchId = 2;
|
||||
|
||||
public browserRendererId: number;
|
||||
|
||||
public logger: ILogger;
|
||||
public logger: Logger;
|
||||
|
||||
public constructor(browserRendererId: number, logger: ILogger) {
|
||||
public constructor(browserRendererId: number, logger: Logger) {
|
||||
this.browserRendererId = browserRendererId;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public static getOrCreateQueue(browserRendererId: number, logger: ILogger): RenderQueue {
|
||||
public static getOrCreateQueue(browserRendererId: number, logger: Logger): RenderQueue {
|
||||
const queue = this.renderQueues.get(browserRendererId);
|
||||
if (queue) {
|
||||
return queue;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export enum LogLevel {
|
|||
}
|
||||
|
||||
/** An abstraction that provides a sink for diagnostic messages. */
|
||||
export interface ILogger { // eslint-disable-line @typescript-eslint/interface-name-prefix
|
||||
export interface Logger { // eslint-disable-line @typescript-eslint/interface-name-prefix
|
||||
/** Called by the framework to emit a diagnostic message.
|
||||
*
|
||||
* @param {LogLevel} logLevel The severity level of the message.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import { ILogger, LogLevel } from './ILogger';
|
||||
import { Logger, LogLevel } from './Logger';
|
||||
|
||||
export class NullLogger implements ILogger {
|
||||
public static instance: ILogger = new NullLogger();
|
||||
export class NullLogger implements Logger {
|
||||
public static instance: Logger = new NullLogger();
|
||||
|
||||
private constructor() { }
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export class NullLogger implements ILogger {
|
|||
}
|
||||
}
|
||||
|
||||
export class ConsoleLogger implements ILogger {
|
||||
export class ConsoleLogger implements Logger {
|
||||
private readonly minimumLogLevel: LogLevel;
|
||||
|
||||
public constructor(minimumLogLevel: LogLevel) {
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
|
||||
import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconnectCircuitHandler";
|
||||
import { UserSpecifiedDisplay } from "../src/Platform/Circuits/UserSpecifiedDisplay";
|
||||
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
|
||||
import { ReconnectDisplay } from "../src/Platform/Circuits/ReconnectDisplay";
|
||||
import { NullLogger} from '../src/Platform/Logging/Loggers';
|
||||
import '../src/GlobalExports';
|
||||
|
||||
describe('AutoReconnectCircuitHandler', () => {
|
||||
it('creates default element', () => {
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
|
||||
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||||
expect(handler.reconnectDisplay).toBeInstanceOf(DefaultReconnectDisplay);
|
||||
});
|
||||
|
||||
it('locates user-specified handler', () => {
|
||||
const element = document.createElement('div');
|
||||
element.id = 'components-reconnect-modal';
|
||||
document.body.appendChild(element);
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
|
||||
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||||
expect(handler.reconnectDisplay).toBeInstanceOf(UserSpecifiedDisplay);
|
||||
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
const TestDisplay = jest.fn<ReconnectDisplay, any[]>(() => ({
|
||||
show: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
failed: jest.fn()
|
||||
}));
|
||||
|
||||
it('hides display on connection up', () => {
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
const testDisplay = new TestDisplay();
|
||||
handler.reconnectDisplay = testDisplay;
|
||||
|
||||
handler.onConnectionUp();
|
||||
|
||||
expect(testDisplay.hide).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
it('shows display on connection down', async () => {
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
handler.delay = () => Promise.resolve();
|
||||
const reconnect = jest.fn().mockResolvedValue(true);
|
||||
window['Blazor'].reconnect = reconnect;
|
||||
|
||||
const testDisplay = new TestDisplay();
|
||||
handler.reconnectDisplay = testDisplay;
|
||||
|
||||
await handler.onConnectionDown();
|
||||
|
||||
expect(testDisplay.show).toHaveBeenCalled();
|
||||
expect(testDisplay.failed).not.toHaveBeenCalled();
|
||||
expect(reconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('invokes failed if reconnect fails', async () => {
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
handler.delay = () => Promise.resolve();
|
||||
const reconnect = jest.fn().mockRejectedValue(new Error('some error'));
|
||||
window.console.error = jest.fn();
|
||||
window['Blazor'].reconnect = reconnect;
|
||||
|
||||
const testDisplay = new TestDisplay();
|
||||
handler.reconnectDisplay = testDisplay;
|
||||
|
||||
await handler.onConnectionDown();
|
||||
|
||||
expect(testDisplay.show).toHaveBeenCalled();
|
||||
expect(testDisplay.failed).toHaveBeenCalled();
|
||||
expect(reconnect).toHaveBeenCalledTimes(AutoReconnectCircuitHandler.MaxRetries);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,13 +6,13 @@ describe('DefaultReconnectDisplay', () => {
|
|||
|
||||
it ('adds element to the body on show', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay(testDocument);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||
|
||||
display.show();
|
||||
|
||||
const element = testDocument.body.querySelector('div');
|
||||
expect(element).toBeDefined();
|
||||
expect(element!.id).toBe(AutoReconnectCircuitHandler.DialogId);
|
||||
expect(element!.id).toBe('test-dialog-id');
|
||||
expect(element!.style.display).toBe('block');
|
||||
|
||||
expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
|
||||
|
|
@ -21,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(testDocument);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||
|
||||
display.show();
|
||||
display.show();
|
||||
|
|
@ -31,7 +31,7 @@ describe('DefaultReconnectDisplay', () => {
|
|||
|
||||
it ('hides element', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay(testDocument);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||
|
||||
display.hide();
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ describe('DefaultReconnectDisplay', () => {
|
|||
|
||||
it ('updates message on fail', () => {
|
||||
const testDocument = new JSDOM().window.document;
|
||||
const display = new DefaultReconnectDisplay(testDocument);
|
||||
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||
|
||||
display.show();
|
||||
display.failed();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import '../src/GlobalExports';
|
||||
import { UserSpecifiedDisplay } from '../src/Platform/Circuits/UserSpecifiedDisplay';
|
||||
import { DefaultReconnectionHandler } from '../src/Platform/Circuits/DefaultReconnectionHandler';
|
||||
import { NullLogger} from '../src/Platform/Logging/Loggers';
|
||||
import { resolveOptions, ReconnectionOptions } from "../src/Platform/Circuits/BlazorOptions";
|
||||
import { ReconnectDisplay } from '../src/Platform/Circuits/ReconnectDisplay';
|
||||
|
||||
const defaultReconnectionOptions = resolveOptions().reconnectionOptions;
|
||||
|
||||
describe('DefaultReconnectionHandler', () => {
|
||||
it('toggles user-specified UI on disconnection/connection', () => {
|
||||
const element = attachUserSpecifiedUI(defaultReconnectionOptions);
|
||||
const handler = new DefaultReconnectionHandler(NullLogger.instance);
|
||||
|
||||
// Shows on disconnection
|
||||
handler.onConnectionDown(defaultReconnectionOptions);
|
||||
expect(element.className).toBe(UserSpecifiedDisplay.ShowClassName);
|
||||
|
||||
// Hides on reconnection
|
||||
handler.onConnectionUp();
|
||||
expect(element.className).toBe(UserSpecifiedDisplay.HideClassName);
|
||||
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
it('hides display on connection up, and stops retrying', async () => {
|
||||
const testDisplay = createTestDisplay();
|
||||
const reconnect = jest.fn().mockResolvedValue(true);
|
||||
const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
|
||||
|
||||
handler.onConnectionDown({
|
||||
maxRetries: 1000,
|
||||
retryIntervalMilliseconds: 100,
|
||||
dialogId: 'ignored'
|
||||
});
|
||||
handler.onConnectionUp();
|
||||
|
||||
expect(testDisplay.hide).toHaveBeenCalled();
|
||||
await delay(200);
|
||||
expect(reconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows display on connection down', async () => {
|
||||
const testDisplay = createTestDisplay();
|
||||
const reconnect = jest.fn().mockResolvedValue(true);
|
||||
const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
|
||||
|
||||
handler.onConnectionDown({
|
||||
maxRetries: 1000,
|
||||
retryIntervalMilliseconds: 100,
|
||||
dialogId: 'ignored'
|
||||
});
|
||||
expect(testDisplay.show).toHaveBeenCalled();
|
||||
expect(testDisplay.failed).not.toHaveBeenCalled();
|
||||
expect(reconnect).not.toHaveBeenCalled();
|
||||
|
||||
await delay(150);
|
||||
expect(reconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('invokes failed if reconnect fails', async () => {
|
||||
const testDisplay = createTestDisplay();
|
||||
const reconnect = jest.fn().mockRejectedValue(null);
|
||||
const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
|
||||
window.console.error = jest.fn();
|
||||
|
||||
handler.onConnectionDown({
|
||||
maxRetries: 3,
|
||||
retryIntervalMilliseconds: 20,
|
||||
dialogId: 'ignored'
|
||||
});
|
||||
|
||||
await delay(100);
|
||||
expect(testDisplay.show).toHaveBeenCalled();
|
||||
expect(testDisplay.failed).toHaveBeenCalled();
|
||||
expect(reconnect).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
function attachUserSpecifiedUI(options: ReconnectionOptions): Element {
|
||||
const element = document.createElement('div');
|
||||
element.id = options.dialogId;
|
||||
element.className = UserSpecifiedDisplay.HideClassName;
|
||||
document.body.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function delay(durationMilliseconds: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
|
||||
}
|
||||
|
||||
function createTestDisplay(): ReconnectDisplay {
|
||||
return {
|
||||
show: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
failed: jest.fn()
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
(global as any).DotNet = { attachReviver: jest.fn() };
|
||||
|
||||
import RenderQueue from '../src/Platform/Circuits/RenderQueue';
|
||||
import { RenderQueue } from '../src/Platform/Circuits/RenderQueue';
|
||||
import { NullLogger } from '../src/Platform/Logging/Loggers';
|
||||
import * as signalR from '@aspnet/signalr';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue