Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

55 changes: 18 additions & 37 deletions src/Components/Web.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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.');
}
Expand All @@ -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;
};

Expand All @@ -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';

Expand All @@ -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();
Expand All @@ -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.
Expand All @@ -160,6 +140,7 @@ function unhandledError(connection: signalR.HubConnection, err: Error, logger: I
}

window['Blazor'].start = boot;

if (shouldAutoStart()) {
boot();
}
6 changes: 3 additions & 3 deletions src/Components/Web.JS/src/BootCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

This file was deleted.

40 changes: 40 additions & 0 deletions src/Components/Web.JS/src/Platform/Circuits/BlazorOptions.ts
Original file line number Diff line number Diff line change
@@ -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: (_) => { },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, formtting seems to be off here (4 spaces versus 2)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly in resolveOptions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point but would rather not run the CI again over this. We have more work to do in this area soon and can handle this then.

logLevel: LogLevel.Warning,
reconnectionOptions: {
maxRetries: 5,
retryIntervalMilliseconds: 3000,
dialogId: 'components-reconnect-modal',
},
};
10 changes: 0 additions & 10 deletions src/Components/Web.JS/src/Platform/Circuits/CircuitHandler.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReconnectDisplay } from './ReconnectDisplay';
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';

export class DefaultReconnectDisplay implements ReconnectDisplay {
modal: HTMLDivElement;

Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
10 changes: 5 additions & 5 deletions src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading