Skip to content

Commit e451597

Browse files
API review: Web.JS. Fixes #12229 (#12361)
* 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
1 parent e808a4f commit e451597

16 files changed

+264
-206
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,33 @@ import './GlobalExports';
33
import * as signalR from '@aspnet/signalr';
44
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
55
import { shouldAutoStart } from './BootCommon';
6-
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
7-
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
8-
import RenderQueue from './Platform/Circuits/RenderQueue';
6+
import { RenderQueue } from './Platform/Circuits/RenderQueue';
97
import { ConsoleLogger } from './Platform/Logging/Loggers';
10-
import { LogLevel, ILogger } from './Platform/Logging/ILogger';
8+
import { LogLevel, Logger } from './Platform/Logging/Logger';
119
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
1210
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
13-
14-
15-
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
16-
interface BlazorOptions {
17-
configureSignalR: SignalRBuilder;
18-
logLevel: LogLevel;
19-
}
11+
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
12+
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
2013

2114
let renderingFailed = false;
2215
let started = false;
2316

2417
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
25-
2618
if (started) {
2719
throw new Error('Blazor has already started.');
2820
}
2921
started = true;
3022

31-
const defaultOptions: BlazorOptions = {
32-
configureSignalR: (_) => { },
33-
logLevel: LogLevel.Warning,
34-
};
35-
36-
const options: BlazorOptions = { ...defaultOptions, ...userOptions };
37-
38-
// For development.
39-
// Simply put a break point here and modify the log level during
40-
// development to get traces.
41-
// In the future we will allow for users to configure this.
23+
// Establish options to be used
24+
const options = resolveOptions(userOptions);
4225
const logger = new ConsoleLogger(options.logLevel);
43-
26+
window['Blazor'].defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
27+
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
4428
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
4529

46-
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
47-
window['Blazor'].circuitHandlers = circuitHandlers;
48-
49-
// pass options.configureSignalR to configure the signalR.HubConnectionBuilder
50-
const initialConnection = await initializeConnection(options, circuitHandlers, logger);
51-
30+
// Initialize statefully prerendered circuits and their components
31+
// Note: This will all be removed soon
32+
const initialConnection = await initializeConnection(options, logger);
5233
const circuits = discoverPrerenderedCircuits(document);
5334
for (let i = 0; i < circuits.length; i++) {
5435
const circuit = circuits[i];
@@ -59,7 +40,6 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
5940
}
6041

6142
const circuit = await startCircuit(initialConnection);
62-
6343
if (!circuit) {
6444
logger.log(LogLevel.Information, 'No preregistered components to render.');
6545
}
@@ -69,14 +49,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
6949
// We can't reconnect after a failure, so exit early.
7050
return false;
7151
}
72-
const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, logger);
52+
const reconnection = existingConnection || await initializeConnection(options, logger);
7353
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
7454

7555
if (reconnectionFailed(results)) {
7656
return false;
7757
}
7858

79-
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
59+
options.reconnectionHandler!.onConnectionUp();
60+
8061
return true;
8162
};
8263

@@ -97,8 +78,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
9778
}
9879
}
9980

100-
async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
101-
81+
async function initializeConnection(options: BlazorOptions, logger: Logger): Promise<signalR.HubConnection> {
10282
const hubProtocol = new MessagePackHubProtocol();
10383
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
10484

@@ -124,7 +104,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
124104
queue.processBatch(batchId, batchData, connection);
125105
});
126106

127-
connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
107+
connection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
128108
connection.on('JS.Error', error => unhandledError(connection, error, logger));
129109

130110
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
@@ -147,7 +127,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
147127
return connection;
148128
}
149129

150-
function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
130+
function unhandledError(connection: signalR.HubConnection, err: Error, logger: Logger): void {
151131
logger.log(LogLevel.Error, err);
152132

153133
// Disconnect on errors.
@@ -160,6 +140,7 @@ function unhandledError(connection: signalR.HubConnection, err: Error, logger: I
160140
}
161141

162142
window['Blazor'].start = boot;
143+
163144
if (shouldAutoStart()) {
164145
boot();
165146
}

src/Components/Web.JS/src/BootCommon.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface BootJsonData {
4040

4141
// Tells you if the script was added without <script src="..." autostart="false"></script>
4242
export function shouldAutoStart() {
43-
return document &&
43+
return !!(document &&
4444
document.currentScript &&
45-
document.currentScript.getAttribute('autostart') !== 'false';
46-
}
45+
document.currentScript.getAttribute('autostart') !== 'false');
46+
}

src/Components/Web.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { LogLevel } from '../Logging/Logger';
2+
3+
export interface BlazorOptions {
4+
configureSignalR: (builder: signalR.HubConnectionBuilder) => void;
5+
logLevel: LogLevel;
6+
reconnectionOptions: ReconnectionOptions;
7+
reconnectionHandler?: ReconnectionHandler;
8+
}
9+
10+
export function resolveOptions(userOptions?: Partial<BlazorOptions>): BlazorOptions {
11+
const result = { ...defaultOptions, ...userOptions };
12+
13+
// The spread operator can't be used for a deep merge, so do the same for subproperties
14+
if (userOptions && userOptions.reconnectionOptions) {
15+
result.reconnectionOptions = { ...defaultOptions.reconnectionOptions, ...userOptions.reconnectionOptions };
16+
}
17+
18+
return result;
19+
}
20+
21+
export interface ReconnectionOptions {
22+
maxRetries: number;
23+
retryIntervalMilliseconds: number;
24+
dialogId: string;
25+
}
26+
27+
export interface ReconnectionHandler {
28+
onConnectionDown(options: ReconnectionOptions, error?: Error): void;
29+
onConnectionUp(): void;
30+
}
31+
32+
const defaultOptions: BlazorOptions = {
33+
configureSignalR: (_) => { },
34+
logLevel: LogLevel.Warning,
35+
reconnectionOptions: {
36+
maxRetries: 5,
37+
retryIntervalMilliseconds: 3000,
38+
dialogId: 'components-reconnect-modal',
39+
},
40+
};

src/Components/Web.JS/src/Platform/Circuits/CircuitHandler.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReconnectDisplay } from './ReconnectDisplay';
2-
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
2+
33
export class DefaultReconnectDisplay implements ReconnectDisplay {
44
modal: HTMLDivElement;
55

@@ -9,9 +9,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
99

1010
addedToDom: boolean = false;
1111

12-
constructor(private document: Document) {
12+
constructor(dialogId: string, private document: Document) {
1313
this.modal = this.document.createElement('div');
14-
this.modal.id = AutoReconnectCircuitHandler.DialogId;
14+
this.modal.id = dialogId;
1515

1616
const modalStyles = [
1717
'position: fixed',
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ReconnectionHandler, ReconnectionOptions } from './BlazorOptions';
2+
import { ReconnectDisplay } from './ReconnectDisplay';
3+
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
4+
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
5+
import { Logger, LogLevel } from '../Logging/Logger';
6+
7+
export class DefaultReconnectionHandler implements ReconnectionHandler {
8+
private readonly _logger: Logger;
9+
private readonly _overrideDisplay?: ReconnectDisplay;
10+
private readonly _reconnectCallback: () => Promise<boolean>;
11+
private _currentReconnectionProcess: ReconnectionProcess | null = null;
12+
13+
constructor(logger: Logger, overrideDisplay?: ReconnectDisplay, reconnectCallback?: () => Promise<boolean>) {
14+
this._logger = logger;
15+
this._overrideDisplay = overrideDisplay;
16+
this._reconnectCallback = reconnectCallback || (() => window['Blazor'].reconnect());
17+
}
18+
19+
onConnectionDown (options: ReconnectionOptions, error?: Error) {
20+
if (!this._currentReconnectionProcess) {
21+
this._currentReconnectionProcess = new ReconnectionProcess(options, this._logger, this._reconnectCallback, this._overrideDisplay);
22+
}
23+
}
24+
25+
onConnectionUp() {
26+
if (this._currentReconnectionProcess) {
27+
this._currentReconnectionProcess.dispose();
28+
this._currentReconnectionProcess = null;
29+
}
30+
}
31+
};
32+
33+
class ReconnectionProcess {
34+
readonly reconnectDisplay: ReconnectDisplay;
35+
isDisposed = false;
36+
37+
constructor(options: ReconnectionOptions, private logger: Logger, private reconnectCallback: () => Promise<boolean>, display?: ReconnectDisplay) {
38+
const modal = document.getElementById(options.dialogId);
39+
this.reconnectDisplay = display || (modal
40+
? new UserSpecifiedDisplay(modal)
41+
: new DefaultReconnectDisplay(options.dialogId, document));
42+
43+
this.reconnectDisplay.show();
44+
this.attemptPeriodicReconnection(options);
45+
}
46+
47+
public dispose() {
48+
this.isDisposed = true;
49+
this.reconnectDisplay.hide();
50+
}
51+
52+
async attemptPeriodicReconnection(options: ReconnectionOptions) {
53+
for (let i = 0; i < options.maxRetries; i++) {
54+
await this.delay(options.retryIntervalMilliseconds);
55+
if (this.isDisposed) {
56+
break;
57+
}
58+
59+
try {
60+
// reconnectCallback will asynchronously return:
61+
// - true to mean success
62+
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
63+
// - exception to mean we didn't reach the server (this can be sync or async)
64+
const result = await this.reconnectCallback();
65+
if (!result) {
66+
// If the server responded and refused to reconnect, stop auto-retrying.
67+
break;
68+
}
69+
return;
70+
} catch (err) {
71+
// We got an exception so will try again momentarily
72+
this.logger.log(LogLevel.Error, err);
73+
}
74+
}
75+
76+
this.reconnectDisplay.failed();
77+
}
78+
79+
delay(durationMilliseconds: number): Promise<void> {
80+
return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
81+
}
82+
}

src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { renderBatch } from '../../Rendering/Renderer';
22
import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
3-
import { ILogger, LogLevel } from '../Logging/ILogger';
3+
import { Logger, LogLevel } from '../Logging/Logger';
44
import { HubConnection } from '@aspnet/signalr';
55

6-
export default class RenderQueue {
6+
export class RenderQueue {
77
private static renderQueues = new Map<number, RenderQueue>();
88

99
private nextBatchId = 2;
1010

1111
public browserRendererId: number;
1212

13-
public logger: ILogger;
13+
public logger: Logger;
1414

15-
public constructor(browserRendererId: number, logger: ILogger) {
15+
public constructor(browserRendererId: number, logger: Logger) {
1616
this.browserRendererId = browserRendererId;
1717
this.logger = logger;
1818
}
1919

20-
public static getOrCreateQueue(browserRendererId: number, logger: ILogger): RenderQueue {
20+
public static getOrCreateQueue(browserRendererId: number, logger: Logger): RenderQueue {
2121
const queue = this.renderQueues.get(browserRendererId);
2222
if (queue) {
2323
return queue;

0 commit comments

Comments
 (0)