-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[Components] Relayer + Robust reconnect #8911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} | ||
}; | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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.'); | ||
} | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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() | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
|
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; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.