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' ;
54import { 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' ;
105import { 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' ;
138import { resolveOptions , CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions' ;
149import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler' ;
15- import { attachRootComponentToLogicalElement } from './Rendering/Renderer' ;
1610import { discoverPersistedState , ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery' ;
17- import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop' ;
1811import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server' ;
19- import { WebRendererId } from './Rendering/WebRendererId' ;
2012import { RootComponentManager } from './Services/RootComponentManager' ;
21- import { detachWebRendererInterop } from './Rendering/WebRendererInteropMethods' ;
22- import { CircuitDotNetCallDispatcher } from './Platform/Circuits/CircuitDotNetCallDispatcher' ;
2313
24- let renderingFailed = false ;
2514let 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 ;
3317let options : CircuitStartOptions ;
3418let logger : ConsoleLogger ;
35- let afterRenderCallback : ( ( ) => void ) | undefined ;
3619
3720export 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
169104export 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}
0 commit comments