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
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Razor" ProjectPath="$(RepositoryRoot)src\Razor\Razor\src\Microsoft.AspNetCore.Razor.csproj" RefProjectPath="$(RepositoryRoot)src\Razor\Razor\ref\Microsoft.AspNetCore.Razor.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Abstractions" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\src\Microsoft.AspNetCore.Mvc.Abstractions.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\ref\Microsoft.AspNetCore.Mvc.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.ApiExplorer" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\src\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\ref\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\ref\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Core" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\ref\Microsoft.AspNetCore.Mvc.Core.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Cors" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\src\Microsoft.AspNetCore.Mvc.Cors.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\ref\Microsoft.AspNetCore.Mvc.Cors.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.DataAnnotations" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\src\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\ref\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public partial class WebAssemblyUriHelper : Microsoft.AspNetCore.Components.Serv
{
internal WebAssemblyUriHelper() { }
public static readonly Microsoft.AspNetCore.Blazor.Services.WebAssemblyUriHelper Instance;
protected override void InitializeState() { }
protected override void EnsureInitialized() { }
protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string newAbsoluteUri) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ internal WebAssemblyUriHelper()
{
}

/// <summary>
/// Called to initialize BaseURI and current URI before those values are used the first time.
/// Override this method to dynamically calculate those values.
/// </summary>
protected override void InitializeState()
protected override void EnsureInitialized()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(
Interop.EnableNavigationInterception,
Expand All @@ -40,8 +36,7 @@ protected override void InitializeState()
// client-side (Mono) use, so it's OK to rely on synchronicity here.
var baseUri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetBaseUri);
var uri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetLocationHref);
SetAbsoluteBaseUri(baseUri);
SetAbsoluteUri(uri);
InitializeState(uri, baseUri);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ public void AttachComponent(IComponent component)

protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
ExceptionDispatchInfo.Capture(exception).Throw();
}

protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
Expand Down
9 changes: 6 additions & 3 deletions src/Components/Browser.JS/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
"@aspnet/signalr": "^1.0.0",
"@aspnet/signalr-protocol-msgpack": "^1.0.0",
"@dotnet/jsinterop": "^0.1.1",
"@types/jsdom": "11.0.6",
"@types/jest": "^24.0.6",
"@types/emscripten": "0.0.31",
"@types/jest": "^24.0.6",
"@types/jsdom": "11.0.6",
"@typescript-eslint/eslint-plugin": "^1.5.0",
"@typescript-eslint/parser": "^1.5.0",
"eslint": "^5.16.0",
"jest": "^24.1.0",
"ts-jest": "^24.0.0",
"ts-loader": "^4.4.1",
"typescript": "^2.9.2",
"typescript": "^3.4.0",
"webpack": "^4.12.0",
"webpack-cli": "^3.0.8"
}
Expand Down
40 changes: 40 additions & 0 deletions src/Components/Browser.JS/src/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
],
env: {
browser: true,
es6: true,
},
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-use-before-define": [ "off" ],
"no-var": "error",
"prefer-const": "error",
"quotes": ["error", "single", { "avoidEscape": true }],
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"semi-spacing": ["error", { "after": true }],
"spaced-comment": ["error", "always"],
"unicode-bom": ["error", "never"],
"brace-style": ["error", "1tbs"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore"
}],
"comma-style": ["error", "last"],
"comma-spacing": ["error", { "after": true }],
"no-trailing-spaces": ["error"]
},
globals: {
DotNet: "readonly"
}
};
102 changes: 70 additions & 32 deletions src/Components/Browser.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,86 @@ import '@dotnet/jsinterop';
import './GlobalExports';
import * as signalR from '@aspnet/signalr';
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
import { OutOfProcessRenderBatch } from './Rendering/RenderBatch/OutOfProcessRenderBatch';
import { internalFunctions as uriHelperFunctions } from './Services/UriHelper';
import { renderBatch } from './Rendering/Renderer';
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync } from './BootCommon';
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
import RenderQueue from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, ILogger } from './Platform/Logging/ILogger';
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';

async function boot() {
const circuitHandlers: CircuitHandler[] = [ new AutoReconnectCircuitHandler() ];
let renderingFailed = false;

async function boot(): Promise<void> {

// 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(LogLevel.Error);

logger.log(LogLevel.Information, 'Booting blazor.');

const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
window['Blazor'].circuitHandlers = circuitHandlers;

// In the background, start loading the boot config and any embedded resources
const embeddedResourcesPromise = fetchBootConfigAsync().then(bootConfig => {
return loadEmbeddedResourcesAsync(bootConfig);
});

const initialConnection = await initializeConnection(circuitHandlers);
const initialConnection = await initializeConnection(circuitHandlers, logger);

const circuits = discoverPrerenderedCircuits(document);
for (let i = 0; i < circuits.length; i++) {
const circuit = circuits[i];
for (let j = 0; j < circuit.components.length; j++) {
const component = circuit.components[j];
component.initialize();
}
}

// Ensure any embedded resources have been loaded before starting the app
await embeddedResourcesPromise;
const circuitId = await initialConnection.invoke<string>(
'StartCircuit',
uriHelperFunctions.getLocationHref(),
uriHelperFunctions.getBaseURI()
);

window['Blazor'].reconnect = async () => {
const reconnection = await initializeConnection(circuitHandlers);
if (!(await reconnection.invoke<Boolean>('ConnectCircuit', circuitId))) {

const circuit = await startCircuit(initialConnection);

if (!circuit) {
logger.log(LogLevel.Information, 'No preregistered components to render.');
}

const reconnect = async (): Promise<boolean> => {
if (renderingFailed) {
// We can't reconnect after a failure, so exit early.
return false;
}
const reconnection = await initializeConnection(circuitHandlers, logger);
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));

if (reconnectionFailed(results)) {
return false;
}

circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
return true;
};

circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
window['Blazor'].reconnect = reconnect;

const reconnectTask = reconnect();

if (circuit) {
circuits.push(circuit);
}

await reconnectTask;

function reconnectionFailed(results: boolean[]): boolean {
return !results.reduce((current, next) => current && next, true);
}
}

async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
async function initializeConnection(circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as any).name = 'blazorpack';
const connection = new signalR.HubConnectionBuilder()
Expand All @@ -51,44 +91,42 @@ async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<
.build();

connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
connection.on('JS.RenderBatch', (browserRendererId: number, renderId: number, batchData: Uint8Array) => {
try {
renderBatch(browserRendererId, new OutOfProcessRenderBatch(batchData));
connection.send('OnRenderCompleted', renderId, null);
} catch (ex) {
// If there's a rendering exception, notify server *and* throw on client
connection.send('OnRenderCompleted', renderId, ex.toString());
throw ex;
}
connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => {
logger.log(LogLevel.Information, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`);

const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger);

queue.processBatch(batchId, batchData, connection);
});

connection.onclose(error => circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
connection.on('JS.Error', error => unhandledError(connection, error));
connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
connection.on('JS.Error', error => unhandledError(connection, error, logger));

window['Blazor']._internal.forceCloseConnection = () => connection.stop();

try {
await connection.start();
} catch (ex) {
unhandledError(connection, ex);
unhandledError(connection, ex, logger);
}

DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
}
},
});

return connection;
}

function unhandledError(connection: signalR.HubConnection, err: Error) {
console.error(err);
function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
logger.log(LogLevel.Error, err);

// Disconnect on errors.
//
// Trying to call methods on the connection after its been closed will throw.
if (connection) {
renderingFailed = true;
Copy link
Member

Choose a reason for hiding this comment

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

@rynowak Your idea for displaying an error UI would be extremely valuable here. @javiercn and I were discussing this earlier.

As it stands, if there's a rendering error, the app will disconnect and stop responding, but the user won't be given any visual indication of what's going on. The UI will just freeze.

I think that's OK for right now, but makes me think we should prioritise including the error UI.

connection.stop();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ 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 {
static readonly MaxRetries = 5;
static readonly RetryInterval = 3000;
static readonly DialogId = 'components-reconnect-modal';
reconnectDisplay: ReconnectDisplay;
public static readonly MaxRetries = 5;
public static readonly RetryInterval = 3000;
public static readonly DialogId = 'components-reconnect-modal';
public reconnectDisplay: ReconnectDisplay;
public logger: ILogger;

constructor() {
public constructor(logger: ILogger) {
this.logger = logger;
this.reconnectDisplay = new DefaultReconnectDisplay(document);
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
Expand All @@ -17,15 +20,15 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
}
});
}
onConnectionUp() : void{
public onConnectionUp(): void {
this.reconnectDisplay.hide();
}

delay() : Promise<void>{
public delay(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
}

async onConnectionDown() : Promise<void> {
public async onConnectionDown(): Promise<void> {
this.reconnectDisplay.show();

for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
Expand All @@ -38,7 +41,7 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
}
return;
} catch (err) {
console.error(err);
this.logger.log(LogLevel.Error, err);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export interface CircuitHandler {
/** Invoked when a server connection is established or re-established after a connection failure.
/** Invoked when a server connection is established or re-established after a connection failure.
*/
onConnectionUp?() : void;
onConnectionUp?(): void;

/** Invoked when a server connection is dropped.
/** 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;
onConnectionDown?(error?: Error): void;
}
Loading