* 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 * as signalR from '@aspnet/signalr';
|
||||||
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
|
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
|
||||||
import { shouldAutoStart } from './BootCommon';
|
import { shouldAutoStart } from './BootCommon';
|
||||||
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
|
import { RenderQueue } from './Platform/Circuits/RenderQueue';
|
||||||
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
|
|
||||||
import RenderQueue from './Platform/Circuits/RenderQueue';
|
|
||||||
import { ConsoleLogger } from './Platform/Logging/Loggers';
|
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 { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
|
||||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||||
|
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
|
||||||
|
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||||
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
|
|
||||||
interface BlazorOptions {
|
|
||||||
configureSignalR: SignalRBuilder;
|
|
||||||
logLevel: LogLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
let renderingFailed = false;
|
let renderingFailed = false;
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||||
|
|
||||||
if (started) {
|
if (started) {
|
||||||
throw new Error('Blazor has already started.');
|
throw new Error('Blazor has already started.');
|
||||||
}
|
}
|
||||||
started = true;
|
started = true;
|
||||||
|
|
||||||
const defaultOptions: BlazorOptions = {
|
// Establish options to be used
|
||||||
configureSignalR: (_) => { },
|
const options = resolveOptions(userOptions);
|
||||||
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.
|
|
||||||
const logger = new ConsoleLogger(options.logLevel);
|
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.');
|
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
|
||||||
|
|
||||||
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
|
// Initialize statefully prerendered circuits and their components
|
||||||
window['Blazor'].circuitHandlers = circuitHandlers;
|
// Note: This will all be removed soon
|
||||||
|
const initialConnection = await initializeConnection(options, logger);
|
||||||
// pass options.configureSignalR to configure the signalR.HubConnectionBuilder
|
|
||||||
const initialConnection = await initializeConnection(options, circuitHandlers, logger);
|
|
||||||
|
|
||||||
const circuits = discoverPrerenderedCircuits(document);
|
const circuits = discoverPrerenderedCircuits(document);
|
||||||
for (let i = 0; i < circuits.length; i++) {
|
for (let i = 0; i < circuits.length; i++) {
|
||||||
const circuit = circuits[i];
|
const circuit = circuits[i];
|
||||||
|
|
@ -59,7 +40,6 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const circuit = await startCircuit(initialConnection);
|
const circuit = await startCircuit(initialConnection);
|
||||||
|
|
||||||
if (!circuit) {
|
if (!circuit) {
|
||||||
logger.log(LogLevel.Information, 'No preregistered components to render.');
|
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.
|
// We can't reconnect after a failure, so exit early.
|
||||||
return false;
|
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)));
|
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
|
||||||
|
|
||||||
if (reconnectionFailed(results)) {
|
if (reconnectionFailed(results)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
|
options.reconnectionHandler!.onConnectionUp();
|
||||||
|
|
||||||
return true;
|
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();
|
const hubProtocol = new MessagePackHubProtocol();
|
||||||
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
|
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
|
||||||
|
|
||||||
|
|
@ -124,7 +104,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
|
||||||
queue.processBatch(batchId, batchData, connection);
|
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));
|
connection.on('JS.Error', error => unhandledError(connection, error, logger));
|
||||||
|
|
||||||
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
|
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
|
||||||
|
|
@ -147,7 +127,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
|
||||||
return connection;
|
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);
|
logger.log(LogLevel.Error, err);
|
||||||
|
|
||||||
// Disconnect on errors.
|
// Disconnect on errors.
|
||||||
|
|
@ -160,6 +140,7 @@ function unhandledError(connection: signalR.HubConnection, err: Error, logger: I
|
||||||
}
|
}
|
||||||
|
|
||||||
window['Blazor'].start = boot;
|
window['Blazor'].start = boot;
|
||||||
|
|
||||||
if (shouldAutoStart()) {
|
if (shouldAutoStart()) {
|
||||||
boot();
|
boot();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ interface BootJsonData {
|
||||||
|
|
||||||
// Tells you if the script was added without <script src="..." autostart="false"></script>
|
// Tells you if the script was added without <script src="..." autostart="false"></script>
|
||||||
export function shouldAutoStart() {
|
export function shouldAutoStart() {
|
||||||
return document &&
|
return !!(document &&
|
||||||
document.currentScript &&
|
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 { ReconnectDisplay } from './ReconnectDisplay';
|
||||||
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
|
|
||||||
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
modal: HTMLDivElement;
|
modal: HTMLDivElement;
|
||||||
|
|
||||||
|
|
@ -9,9 +9,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||||
|
|
||||||
addedToDom: boolean = false;
|
addedToDom: boolean = false;
|
||||||
|
|
||||||
constructor(private document: Document) {
|
constructor(dialogId: string, private document: Document) {
|
||||||
this.modal = this.document.createElement('div');
|
this.modal = this.document.createElement('div');
|
||||||
this.modal.id = AutoReconnectCircuitHandler.DialogId;
|
this.modal.id = dialogId;
|
||||||
|
|
||||||
const modalStyles = [
|
const modalStyles = [
|
||||||
'position: fixed',
|
'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 { renderBatch } from '../../Rendering/Renderer';
|
||||||
import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
|
import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
|
||||||
import { ILogger, LogLevel } from '../Logging/ILogger';
|
import { Logger, LogLevel } from '../Logging/Logger';
|
||||||
import { HubConnection } from '@aspnet/signalr';
|
import { HubConnection } from '@aspnet/signalr';
|
||||||
|
|
||||||
export default class RenderQueue {
|
export class RenderQueue {
|
||||||
private static renderQueues = new Map<number, RenderQueue>();
|
private static renderQueues = new Map<number, RenderQueue>();
|
||||||
|
|
||||||
private nextBatchId = 2;
|
private nextBatchId = 2;
|
||||||
|
|
||||||
public browserRendererId: number;
|
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.browserRendererId = browserRendererId;
|
||||||
this.logger = logger;
|
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);
|
const queue = this.renderQueues.get(browserRendererId);
|
||||||
if (queue) {
|
if (queue) {
|
||||||
return queue;
|
return queue;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export enum LogLevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An abstraction that provides a sink for diagnostic messages. */
|
/** 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.
|
/** Called by the framework to emit a diagnostic message.
|
||||||
*
|
*
|
||||||
* @param {LogLevel} logLevel The severity level of the message.
|
* @param {LogLevel} logLevel The severity level of the message.
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { ILogger, LogLevel } from './ILogger';
|
import { Logger, LogLevel } from './Logger';
|
||||||
|
|
||||||
export class NullLogger implements ILogger {
|
export class NullLogger implements Logger {
|
||||||
public static instance: ILogger = new NullLogger();
|
public static instance: Logger = new NullLogger();
|
||||||
|
|
||||||
private constructor() { }
|
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;
|
private readonly minimumLogLevel: LogLevel;
|
||||||
|
|
||||||
public constructor(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', () => {
|
it ('adds element to the body on show', () => {
|
||||||
const testDocument = new JSDOM().window.document;
|
const testDocument = new JSDOM().window.document;
|
||||||
const display = new DefaultReconnectDisplay(testDocument);
|
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||||
|
|
||||||
display.show();
|
display.show();
|
||||||
|
|
||||||
const element = testDocument.body.querySelector('div');
|
const element = testDocument.body.querySelector('div');
|
||||||
expect(element).toBeDefined();
|
expect(element).toBeDefined();
|
||||||
expect(element!.id).toBe(AutoReconnectCircuitHandler.DialogId);
|
expect(element!.id).toBe('test-dialog-id');
|
||||||
expect(element!.style.display).toBe('block');
|
expect(element!.style.display).toBe('block');
|
||||||
|
|
||||||
expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
|
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', () => {
|
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(testDocument);
|
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||||
|
|
||||||
display.show();
|
display.show();
|
||||||
display.show();
|
display.show();
|
||||||
|
|
@ -31,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(testDocument);
|
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||||
|
|
||||||
display.hide();
|
display.hide();
|
||||||
|
|
||||||
|
|
@ -40,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(testDocument);
|
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
|
||||||
|
|
||||||
display.show();
|
display.show();
|
||||||
display.failed();
|
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() };
|
(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 { NullLogger } from '../src/Platform/Logging/Loggers';
|
||||||
import * as signalR from '@aspnet/signalr';
|
import * as signalR from '@aspnet/signalr';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue