|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +import { DotNet } from '@microsoft/dotnet-js-interop'; |
| 5 | +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 | +import { ConsoleLogger } from './Platform/Logging/Loggers'; |
| 11 | +import { LogLevel, Logger } from './Platform/Logging/Logger'; |
| 12 | +import { CircuitDescriptor } from './Platform/Circuits/CircuitManager'; |
| 13 | +import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions'; |
| 14 | +import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler'; |
| 15 | +import { attachRootComponentToLogicalElement } from './Rendering/Renderer'; |
| 16 | +import { discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery'; |
| 17 | +import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop'; |
| 18 | +import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server'; |
| 19 | + |
| 20 | +let renderingFailed = false; |
| 21 | +let connection: HubConnection; |
| 22 | +let circuit: CircuitDescriptor; |
| 23 | +let dispatcher: DotNet.ICallDispatcher; |
| 24 | + |
| 25 | +export async function startCircuit(userOptions?: Partial<CircuitStartOptions>, components?: ServerComponentDescriptor[]): Promise<void> { |
| 26 | + // Establish options to be used |
| 27 | + const options = resolveOptions(userOptions); |
| 28 | + const jsInitializer = await fetchAndInvokeInitializers(options); |
| 29 | + |
| 30 | + const logger = new ConsoleLogger(options.logLevel); |
| 31 | + |
| 32 | + Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => { |
| 33 | + if (renderingFailed) { |
| 34 | + // We can't reconnect after a failure, so exit early. |
| 35 | + return false; |
| 36 | + } |
| 37 | + |
| 38 | + const reconnection = existingConnection || await initializeConnection(options, logger, circuit); |
| 39 | + if (!(await circuit.reconnect(reconnection))) { |
| 40 | + 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.'); |
| 41 | + return false; |
| 42 | + } |
| 43 | + |
| 44 | + options.reconnectionHandler!.onConnectionUp(); |
| 45 | + |
| 46 | + return true; |
| 47 | + }; |
| 48 | + Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger); |
| 49 | + |
| 50 | + options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler; |
| 51 | + logger.log(LogLevel.Information, 'Starting up Blazor server-side application.'); |
| 52 | + |
| 53 | + const appState = discoverPersistedState(document); |
| 54 | + circuit = new CircuitDescriptor(components || [], appState || ''); |
| 55 | + |
| 56 | + // Configure navigation via SignalR |
| 57 | + Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => { |
| 58 | + return connection.send('OnLocationChanged', uri, state, intercepted); |
| 59 | + }, (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => { |
| 60 | + return connection.send('OnLocationChanging', callId, uri, state, intercepted); |
| 61 | + }); |
| 62 | + |
| 63 | + Blazor._internal.forceCloseConnection = () => connection.stop(); |
| 64 | + Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize); |
| 65 | + |
| 66 | + dispatcher = DotNet.attachDispatcher({ |
| 67 | + beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => { |
| 68 | + connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson); |
| 69 | + }, |
| 70 | + endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => { |
| 71 | + connection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson); |
| 72 | + }, |
| 73 | + sendByteArray: (id: number, data: Uint8Array): void => { |
| 74 | + connection.send('ReceiveByteArray', id, data); |
| 75 | + }, |
| 76 | + }); |
| 77 | + |
| 78 | + const initialConnection = await initializeConnection(options, logger, circuit); |
| 79 | + const circuitStarted = await circuit.startCircuit(initialConnection); |
| 80 | + if (!circuitStarted) { |
| 81 | + logger.log(LogLevel.Error, 'Failed to start the circuit.'); |
| 82 | + return; |
| 83 | + } |
| 84 | + |
| 85 | + let disconnectSent = false; |
| 86 | + const cleanup = () => { |
| 87 | + if (!disconnectSent) { |
| 88 | + const data = new FormData(); |
| 89 | + const circuitId = circuit.circuitId!; |
| 90 | + data.append('circuitId', circuitId); |
| 91 | + disconnectSent = navigator.sendBeacon('_blazor/disconnect', data); |
| 92 | + } |
| 93 | + }; |
| 94 | + |
| 95 | + Blazor.disconnect = cleanup; |
| 96 | + |
| 97 | + window.addEventListener('unload', cleanup, { capture: false, once: true }); |
| 98 | + |
| 99 | + logger.log(LogLevel.Information, 'Blazor server-side application started.'); |
| 100 | + |
| 101 | + jsInitializer.invokeAfterStartedCallbacks(Blazor); |
| 102 | +} |
| 103 | + |
| 104 | +async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> { |
| 105 | + const hubProtocol = new MessagePackHubProtocol(); |
| 106 | + (hubProtocol as unknown as { name: string }).name = 'blazorpack'; |
| 107 | + |
| 108 | + const connectionBuilder = new HubConnectionBuilder() |
| 109 | + .withUrl('_blazor') |
| 110 | + .withHubProtocol(hubProtocol); |
| 111 | + |
| 112 | + options.configureSignalR(connectionBuilder); |
| 113 | + |
| 114 | + const newConnection = connectionBuilder.build(); |
| 115 | + |
| 116 | + newConnection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId, false)); |
| 117 | + newConnection.on('JS.BeginInvokeJS', dispatcher.beginInvokeJSFromDotNet.bind(dispatcher)); |
| 118 | + newConnection.on('JS.EndInvokeDotNet', dispatcher.endInvokeDotNetFromJS.bind(dispatcher)); |
| 119 | + newConnection.on('JS.ReceiveByteArray', dispatcher.receiveByteArray.bind(dispatcher)); |
| 120 | + |
| 121 | + newConnection.on('JS.BeginTransmitStream', (streamId: number) => { |
| 122 | + const readableStream = new ReadableStream({ |
| 123 | + start(controller) { |
| 124 | + newConnection.stream('SendDotNetStreamToJS', streamId).subscribe({ |
| 125 | + next: (chunk: Uint8Array) => controller.enqueue(chunk), |
| 126 | + complete: () => controller.close(), |
| 127 | + error: (err) => controller.error(err), |
| 128 | + }); |
| 129 | + }, |
| 130 | + }); |
| 131 | + |
| 132 | + dispatcher.supplyDotNetStream(streamId, readableStream); |
| 133 | + }); |
| 134 | + |
| 135 | + const renderQueue = RenderQueue.getOrCreate(logger); |
| 136 | + newConnection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => { |
| 137 | + logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`); |
| 138 | + renderQueue.processBatch(batchId, batchData, newConnection); |
| 139 | + }); |
| 140 | + |
| 141 | + newConnection.on('JS.EndLocationChanging', Blazor._internal.navigationManager.endLocationChanging); |
| 142 | + |
| 143 | + newConnection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error)); |
| 144 | + newConnection.on('JS.Error', error => { |
| 145 | + renderingFailed = true; |
| 146 | + unhandledError(newConnection, error, logger); |
| 147 | + showErrorNotification(); |
| 148 | + }); |
| 149 | + |
| 150 | + try { |
| 151 | + await newConnection.start(); |
| 152 | + connection = newConnection; |
| 153 | + } catch (ex: any) { |
| 154 | + unhandledError(newConnection, ex as Error, logger); |
| 155 | + |
| 156 | + if (ex.errorType === 'FailedToNegotiateWithServerError') { |
| 157 | + // Connection with the server has been interrupted, and we're in the process of reconnecting. |
| 158 | + // Throw this exception so it can be handled at the reconnection layer, and don't show the |
| 159 | + // error notification. |
| 160 | + throw ex; |
| 161 | + } else { |
| 162 | + showErrorNotification(); |
| 163 | + } |
| 164 | + |
| 165 | + if (ex.innerErrors) { |
| 166 | + if (ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) { |
| 167 | + logger.log(LogLevel.Error, 'Unable to connect, please ensure you are using an updated browser that supports WebSockets.'); |
| 168 | + } else if (ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === HttpTransportType.WebSockets)) { |
| 169 | + logger.log(LogLevel.Error, 'Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.'); |
| 170 | + } else if (ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === HttpTransportType.LongPolling)) { |
| 171 | + 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.'); |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + // Check if the connection is established using the long polling transport, |
| 177 | + // using the `features.inherentKeepAlive` property only present with long polling. |
| 178 | + if ((newConnection as any).connection?.features?.inherentKeepAlive) { |
| 179 | + 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.'); |
| 180 | + } |
| 181 | + |
| 182 | + return newConnection; |
| 183 | +} |
| 184 | + |
| 185 | +function unhandledError(connection: HubConnection, err: Error, logger: Logger): void { |
| 186 | + logger.log(LogLevel.Error, err); |
| 187 | + |
| 188 | + // Disconnect on errors. |
| 189 | + // |
| 190 | + // Trying to call methods on the connection after its been closed will throw. |
| 191 | + if (connection) { |
| 192 | + connection.stop(); |
| 193 | + } |
| 194 | +} |
0 commit comments