Skip to content

Commit d501c5f

Browse files
committed
Move circuit state to CircuitManager
1 parent 855043f commit d501c5f

File tree

7 files changed

+327
-292
lines changed

7 files changed

+327
-292
lines changed

src/Components/Web.JS/dist/Release/blazor.server.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/dist/Release/blazor.web.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.
Lines changed: 33 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,21 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
import { DotNet } from '@microsoft/dotnet-js-interop';
54
import { Blazor } from './GlobalExports';
6-
import { HubConnectionBuilder, HubConnection, HttpTransportType } from '@microsoft/signalr';
7-
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';
8-
import { showErrorNotification } from './BootErrors';
9-
import { RenderQueue } from './Platform/Circuits/RenderQueue';
105
import { ConsoleLogger } from './Platform/Logging/Loggers';
11-
import { LogLevel, Logger } from './Platform/Logging/Logger';
12-
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
6+
import { LogLevel } from './Platform/Logging/Logger';
7+
import { CircuitManager } from './Platform/Circuits/CircuitManager';
138
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
149
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
15-
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
1610
import { discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
17-
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop';
1811
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server';
19-
import { WebRendererId } from './Rendering/WebRendererId';
2012
import { RootComponentManager } from './Services/RootComponentManager';
21-
import { detachWebRendererInterop } from './Rendering/WebRendererInteropMethods';
22-
import { CircuitDotNetCallDispatcher } from './Platform/Circuits/CircuitDotNetCallDispatcher';
2313

24-
let renderingFailed = false;
2514
let started = false;
26-
let circuitActive = false;
27-
let startCircuitPromise: Promise<boolean> | undefined;
28-
let connection: HubConnection;
29-
let circuit: CircuitDescriptor;
30-
let dotNetDispatcher: CircuitDotNetCallDispatcher;
31-
let dispatcher: DotNet.ICallDispatcher;
32-
let renderQueue: RenderQueue;
15+
let appState: string;
16+
let circuit: CircuitManager;
3317
let options: CircuitStartOptions;
3418
let logger: ConsoleLogger;
35-
let afterRenderCallback: (() => void) | undefined;
3619

3720
export function setCircuitOptions(circuitUserOptions?: Partial<CircuitStartOptions>) {
3821
if (options) {
@@ -48,54 +31,44 @@ export async function startServer(components: RootComponentManager<ServerCompone
4831
}
4932

5033
started = true;
51-
52-
// Establish options to be used
34+
appState = discoverPersistedState(document) || '';
5335
logger = new ConsoleLogger(options.logLevel);
36+
circuit = new CircuitManager(components, appState, options, logger);
5437

55-
const jsInitializer = await fetchAndInvokeInitializers(options);
38+
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
5639

57-
Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => {
58-
if (renderingFailed) {
40+
Blazor.reconnect = async () => {
41+
if (circuit.didRenderingFail()) {
5942
// We can't reconnect after a failure, so exit early.
6043
return false;
6144
}
6245

63-
const reconnection = existingConnection || await initializeConnection(logger, circuit);
64-
if (!(await circuit.reconnect(reconnection))) {
46+
if (!(await circuit.reconnect())) {
6547
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
6648
return false;
6749
}
6850

69-
options.reconnectionHandler!.onConnectionUp();
70-
7151
return true;
7252
};
73-
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
7453

54+
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
7555
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
76-
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
7756

7857
// Configure navigation via SignalR
7958
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
80-
return connection.send('OnLocationChanged', uri, state, intercepted);
59+
return circuit.sendLocationChanged(uri, state, intercepted);
8160
}, (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
82-
return connection.send('OnLocationChanging', callId, uri, state, intercepted);
61+
return circuit.sendLocationChanging(callId, uri, state, intercepted);
8362
});
8463

85-
Blazor._internal.forceCloseConnection = () => connection.stop();
86-
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize);
64+
Blazor._internal.forceCloseConnection = () => circuit.disconnect();
65+
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => circuit.sendJsDataStream(data, streamId, chunkSize);
8766

88-
const didCircuitStart = await startCircuit(components);
89-
if (!didCircuitStart) {
90-
return;
91-
}
67+
const jsInitializer = await fetchAndInvokeInitializers(options);
68+
await circuit.start();
9269

93-
let disconnectSent = false;
9470
const cleanup = () => {
95-
if (!disconnectSent && isCircuitActive()) {
96-
const data = getDisconnectFormData();
97-
disconnectSent = navigator.sendBeacon('_blazor/disconnect', data);
98-
}
71+
circuit.sendDisconnectBeacon();
9972
};
10073

10174
Blazor.disconnect = cleanup;
@@ -107,176 +80,35 @@ export async function startServer(components: RootComponentManager<ServerCompone
10780
jsInitializer.invokeAfterStartedCallbacks(Blazor);
10881
}
10982

110-
export function startCircuit(components: RootComponentManager<ServerComponentDescriptor>): Promise<boolean> {
83+
export function startCircuit(): Promise<void> {
11184
if (!started) {
11285
throw new Error('Cannot start the circuit until Blazor Server has started.');
11386
}
11487

115-
circuitActive = true;
116-
startCircuitPromise ??= (async () => {
117-
const appState = discoverPersistedState(document);
118-
renderQueue = new RenderQueue(logger);
119-
circuit = new CircuitDescriptor(components, appState || '');
120-
dotNetDispatcher = new CircuitDotNetCallDispatcher(() => connection);
121-
dispatcher = DotNet.attachDispatcher(dotNetDispatcher);
122-
123-
const initialConnection = await initializeConnection(logger, circuit);
124-
const circuitStarted = await circuit.startCircuit(initialConnection);
125-
if (!circuitStarted) {
126-
logger.log(LogLevel.Error, 'Failed to start the circuit.');
127-
return false;
128-
}
129-
return true;
130-
})();
131-
132-
return startCircuitPromise;
133-
}
134-
135-
export async function disposeCircuit() {
136-
if (!circuitActive) {
137-
return;
88+
if (circuit.didRenderingFail()) {
89+
// We can't start a new circuit after a rendering failure because the renderer
90+
// might be in an invalid state.
91+
return Promise.resolve();
13892
}
13993

140-
circuitActive = false;
141-
142-
await startCircuitPromise;
143-
144-
if (circuitActive) {
145-
// A call to 'startCircuit' was made while we were waiting to dispose the circuit.
146-
// Therefore, we should abort the disposal.
147-
return;
94+
if (circuit.isDisposedOrDisposing()) {
95+
// If the current circuit is no longer available, create a new one.
96+
circuit = new CircuitManager(circuit.getRootComponentManager(), appState, options, logger);
14897
}
14998

150-
// We dispose the .NET dispatcher to prevent it from being used in the future.
151-
// This avoids cases where, for example, .NET object references from a
152-
// disconnected circuit start pointing to .NET objects for a new circuit.
153-
dotNetDispatcher.dispose();
154-
155-
const formData = getDisconnectFormData();
156-
fetch('_blazor/disconnect', {
157-
method: 'POST',
158-
body: formData,
159-
});
160-
161-
connection.stop();
162-
163-
detachWebRendererInterop(WebRendererId.Server);
164-
165-
// Setting this to undefined allows a new circuit to be started in the future.
166-
startCircuitPromise = undefined;
99+
// Start the circuit. If the circuit has already started, this will return the existing
100+
// circuit start promise.
101+
return circuit.start();
167102
}
168103

169104
export function hasStartedServer(): boolean {
170105
return started;
171106
}
172107

173-
export function isCircuitActive(): boolean {
174-
return circuitActive;
175-
}
176-
177-
export function attachCircuitAfterRenderCallback(callback: typeof afterRenderCallback) {
178-
if (afterRenderCallback) {
179-
throw new Error('A Blazor Server after render batch callback was already attached.');
180-
}
181-
182-
afterRenderCallback = callback;
183-
}
184-
185-
function getDisconnectFormData(): FormData {
186-
const data = new FormData();
187-
const circuitId = circuit.circuitId!;
188-
data.append('circuitId', circuitId);
189-
return data;
190-
}
191-
192-
async function initializeConnection(logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> {
193-
const hubProtocol = new MessagePackHubProtocol();
194-
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
195-
196-
const connectionBuilder = new HubConnectionBuilder()
197-
.withUrl('_blazor')
198-
.withHubProtocol(hubProtocol);
199-
200-
options.configureSignalR(connectionBuilder);
201-
202-
const newConnection = connectionBuilder.build();
203-
204-
newConnection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(WebRendererId.Server, circuit.resolveElement(selector, componentId), componentId, false));
205-
newConnection.on('JS.BeginInvokeJS', dispatcher.beginInvokeJSFromDotNet.bind(dispatcher));
206-
newConnection.on('JS.EndInvokeDotNet', dispatcher.endInvokeDotNetFromJS.bind(dispatcher));
207-
newConnection.on('JS.ReceiveByteArray', dispatcher.receiveByteArray.bind(dispatcher));
208-
209-
newConnection.on('JS.BeginTransmitStream', (streamId: number) => {
210-
const readableStream = new ReadableStream({
211-
start(controller) {
212-
newConnection.stream('SendDotNetStreamToJS', streamId).subscribe({
213-
next: (chunk: Uint8Array) => controller.enqueue(chunk),
214-
complete: () => controller.close(),
215-
error: (err) => controller.error(err),
216-
});
217-
},
218-
});
219-
220-
dispatcher.supplyDotNetStream(streamId, readableStream);
221-
});
222-
223-
newConnection.on('JS.RenderBatch', async (batchId: number, batchData: Uint8Array) => {
224-
logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`);
225-
await renderQueue.processBatch(batchId, batchData, newConnection);
226-
afterRenderCallback?.();
227-
});
228-
229-
newConnection.on('JS.EndLocationChanging', Blazor._internal.navigationManager.endLocationChanging);
230-
231-
newConnection.onclose(error => isCircuitActive() && !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
232-
newConnection.on('JS.Error', error => {
233-
renderingFailed = true;
234-
unhandledError(newConnection, error, logger);
235-
showErrorNotification();
236-
});
237-
238-
try {
239-
await newConnection.start();
240-
connection = newConnection;
241-
} catch (ex: any) {
242-
unhandledError(newConnection, ex as Error, logger);
243-
244-
if (ex.errorType === 'FailedToNegotiateWithServerError') {
245-
// Connection with the server has been interrupted, and we're in the process of reconnecting.
246-
// Throw this exception so it can be handled at the reconnection layer, and don't show the
247-
// error notification.
248-
throw ex;
249-
} else {
250-
showErrorNotification();
251-
}
252-
253-
if (ex.innerErrors) {
254-
if (ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) {
255-
logger.log(LogLevel.Error, 'Unable to connect, please ensure you are using an updated browser that supports WebSockets.');
256-
} else if (ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === HttpTransportType.WebSockets)) {
257-
logger.log(LogLevel.Error, 'Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.');
258-
} else if (ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === HttpTransportType.LongPolling)) {
259-
logger.log(LogLevel.Error, 'Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error.');
260-
}
261-
}
262-
}
263-
264-
// Check if the connection is established using the long polling transport,
265-
// using the `features.inherentKeepAlive` property only present with long polling.
266-
if ((newConnection as any).connection?.features?.inherentKeepAlive) {
267-
logger.log(LogLevel.Warning, 'Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling.');
268-
}
269-
270-
return newConnection;
108+
export async function disposeCircuit() {
109+
await circuit?.dispose();
271110
}
272111

273-
function unhandledError(connection: HubConnection, err: Error, logger: Logger): void {
274-
logger.log(LogLevel.Error, err);
275-
276-
// Disconnect on errors.
277-
//
278-
// Trying to call methods on the connection after its been closed will throw.
279-
if (connection) {
280-
connection.stop();
281-
}
112+
export function isCircuitAvailable(): boolean {
113+
return !circuit.isDisposedOrDisposing();
282114
}

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

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

0 commit comments

Comments
 (0)