1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- import { DotNet } from '@microsoft/dotnet-js-interop' ;
5
4
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' ;
10
5
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' ;
13
8
import { resolveOptions , CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions' ;
14
9
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler' ;
15
- import { attachRootComponentToLogicalElement } from './Rendering/Renderer' ;
16
10
import { discoverPersistedState , ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery' ;
17
- import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop' ;
18
11
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server' ;
19
- import { WebRendererId } from './Rendering/WebRendererId' ;
20
12
import { RootComponentManager } from './Services/RootComponentManager' ;
21
- import { detachWebRendererInterop } from './Rendering/WebRendererInteropMethods' ;
22
- import { CircuitDotNetCallDispatcher } from './Platform/Circuits/CircuitDotNetCallDispatcher' ;
23
13
24
- let renderingFailed = false ;
25
14
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 ;
33
17
let options : CircuitStartOptions ;
34
18
let logger : ConsoleLogger ;
35
- let afterRenderCallback : ( ( ) => void ) | undefined ;
36
19
37
20
export function setCircuitOptions ( circuitUserOptions ?: Partial < CircuitStartOptions > ) {
38
21
if ( options ) {
@@ -48,54 +31,44 @@ export async function startServer(components: RootComponentManager<ServerCompone
48
31
}
49
32
50
33
started = true ;
51
-
52
- // Establish options to be used
34
+ appState = discoverPersistedState ( document ) || '' ;
53
35
logger = new ConsoleLogger ( options . logLevel ) ;
36
+ circuit = new CircuitManager ( components , appState , options , logger ) ;
54
37
55
- const jsInitializer = await fetchAndInvokeInitializers ( options ) ;
38
+ logger . log ( LogLevel . Information , 'Starting up Blazor server-side application.' ) ;
56
39
57
- Blazor . reconnect = async ( existingConnection ?: HubConnection ) : Promise < boolean > => {
58
- if ( renderingFailed ) {
40
+ Blazor . reconnect = async ( ) => {
41
+ if ( circuit . didRenderingFail ( ) ) {
59
42
// We can't reconnect after a failure, so exit early.
60
43
return false ;
61
44
}
62
45
63
- const reconnection = existingConnection || await initializeConnection ( logger , circuit ) ;
64
- if ( ! ( await circuit . reconnect ( reconnection ) ) ) {
46
+ if ( ! ( await circuit . reconnect ( ) ) ) {
65
47
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.' ) ;
66
48
return false ;
67
49
}
68
50
69
- options . reconnectionHandler ! . onConnectionUp ( ) ;
70
-
71
51
return true ;
72
52
} ;
73
- Blazor . defaultReconnectionHandler = new DefaultReconnectionHandler ( logger ) ;
74
53
54
+ Blazor . defaultReconnectionHandler = new DefaultReconnectionHandler ( logger ) ;
75
55
options . reconnectionHandler = options . reconnectionHandler || Blazor . defaultReconnectionHandler ;
76
- logger . log ( LogLevel . Information , 'Starting up Blazor server-side application.' ) ;
77
56
78
57
// Configure navigation via SignalR
79
58
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 ) ;
81
60
} , ( 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 ) ;
83
62
} ) ;
84
63
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 ) ;
87
66
88
- const didCircuitStart = await startCircuit ( components ) ;
89
- if ( ! didCircuitStart ) {
90
- return ;
91
- }
67
+ const jsInitializer = await fetchAndInvokeInitializers ( options ) ;
68
+ await circuit . start ( ) ;
92
69
93
- let disconnectSent = false ;
94
70
const cleanup = ( ) => {
95
- if ( ! disconnectSent && isCircuitActive ( ) ) {
96
- const data = getDisconnectFormData ( ) ;
97
- disconnectSent = navigator . sendBeacon ( '_blazor/disconnect' , data ) ;
98
- }
71
+ circuit . sendDisconnectBeacon ( ) ;
99
72
} ;
100
73
101
74
Blazor . disconnect = cleanup ;
@@ -107,176 +80,35 @@ export async function startServer(components: RootComponentManager<ServerCompone
107
80
jsInitializer . invokeAfterStartedCallbacks ( Blazor ) ;
108
81
}
109
82
110
- export function startCircuit ( components : RootComponentManager < ServerComponentDescriptor > ) : Promise < boolean > {
83
+ export function startCircuit ( ) : Promise < void > {
111
84
if ( ! started ) {
112
85
throw new Error ( 'Cannot start the circuit until Blazor Server has started.' ) ;
113
86
}
114
87
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 ( ) ;
138
92
}
139
93
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 ) ;
148
97
}
149
98
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 ( ) ;
167
102
}
168
103
169
104
export function hasStartedServer ( ) : boolean {
170
105
return started ;
171
106
}
172
107
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 ( ) ;
271
110
}
272
111
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 ( ) ;
282
114
}
0 commit comments