Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 51 additions & 88 deletions src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,22 @@ setTimeout(async function () {

let waiting = false;

connection.onmessage = function (message) {
if (message.data === 'Reload') {
console.debug('Server is ready. Reloading...');
location.reload();
} else if (message.data === 'Wait') {
if (waiting) {
return;
}
waiting = true;
console.debug('File changes detected. Waiting for application to rebuild.');
const glyphs = ['☱', '☲', '☴'];
const title = document.title;
let i = 0;
setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240);
connection.onmessage = function (message) {
const payload = JSON.parse(message.data);
const action = {
'Reload': () => reload(),
'Wait': () => wait(),
'UpdateStaticFile': () => updateStaticFile(payload.path),
'ApplyManagedCodeUpdates': () => applyManagedCodeUpdates(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel),
'ReportDiagnostics': () => reportDiagnostics(payload.diagnostics),
'GetApplyUpdateCapabilities': () => getApplyUpdateCapabilities(),
'RefreshBrowser': () => refreshBrowser()
};

if (payload.type && action.hasOwnProperty(payload.type)) {
action[payload.type]();
} else {
const payload = JSON.parse(message.data);
const action = {
'UpdateStaticFile': () => updateStaticFile(payload.path),
'BlazorHotReloadDeltav1': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, false),
'BlazorHotReloadDeltav2': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, true),
'BlazorHotReloadDeltav3': () => applyBlazorDeltas(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel),
'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics),
'BlazorRequestApplyUpdateCapabilities': () => getBlazorWasmApplyUpdateCapabilities(false),
'BlazorRequestApplyUpdateCapabilities2': () => getBlazorWasmApplyUpdateCapabilities(true),
'AspNetCoreHotReloadApplied': () => aspnetCoreHotReloadApplied()
};

if (payload.type && action.hasOwnProperty(payload.type)) {
action[payload.type]();
} else {
console.error('Unknown payload:', message.data);
}
console.error('Unknown payload:', message.data);
}
}

Expand Down Expand Up @@ -106,12 +90,12 @@ setTimeout(async function () {
return messageAndStack
}

function getBlazorWasmApplyUpdateCapabilities(sendErrorToClient) {
function getApplyUpdateCapabilities() {
let applyUpdateCapabilities;
try {
applyUpdateCapabilities = window.Blazor._internal.getApplyUpdateCapabilities();
} catch (error) {
applyUpdateCapabilities = sendErrorToClient ? "!" + getMessageAndStack(error) : '';
applyUpdateCapabilities = "!" + getMessageAndStack(error);
}
connection.send(applyUpdateCapabilities);
}
Expand All @@ -137,41 +121,6 @@ setTimeout(async function () {
styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling);
}

async function applyBlazorDeltas_legacy(serverSecret, deltas, sendErrorToClient) {
if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) {
// Validate the shared secret if it was specified. It might be unspecified in older versions of VS
// that do not support this feature as yet.
throw 'Unable to validate the server. Rejecting apply-update payload.';
}

let applyError = undefined;

try {
applyDeltas_legacy(deltas)
} catch (error) {
console.warn(error);
applyError = error;
}

const body = JSON.stringify({
id: deltas[0].sequenceId,
deltas: deltas
});
try {
await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body });
} catch (error) {
console.warn(error);
applyError = error;
}

if (applyError) {
sendDeltaNotApplied(sendErrorToClient ? applyError : undefined);
} else {
sendDeltaApplied();
notifyHotReloadApplied();
}
}

function applyDeltas_legacy(deltas) {
let apply = window.Blazor?._internal?.applyHotReload

Expand All @@ -190,26 +139,16 @@ setTimeout(async function () {
});
}
}
function sendDeltaApplied() {
connection.send(new Uint8Array([1]).buffer);
}

function sendDeltaNotApplied(error) {
if (error) {
let encoder = new TextEncoder()
connection.send(encoder.encode("\0" + error.message + "\0" + error.stack));
} else {
connection.send(new Uint8Array([0]).buffer);
}
}

async function applyBlazorDeltas(serverSecret, updateId, deltas, responseLoggingLevel) {
async function applyManagedCodeUpdates(serverSecret, updateId, deltas, responseLoggingLevel) {
if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) {
// Validate the shared secret if it was specified. It might be unspecified in older versions of VS
// that do not support this feature as yet.
throw 'Unable to validate the server. Rejecting apply-update payload.';
}

console.debug('Applying managed code updates.');

const AgentMessageSeverity_Error = 2

let applyError = undefined;
Expand Down Expand Up @@ -261,11 +200,13 @@ setTimeout(async function () {
}));

if (!applyError) {
notifyHotReloadApplied();
displayChangesAppliedToast();
}
}

function displayDiagnostics(diagnostics) {
function reportDiagnostics(diagnostics) {
console.debug('Reporting Hot Reload diagnostics.');

document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove());
const el = document.body.appendChild(document.createElement('div'));
el.id = 'dotnet-compile-error';
Expand All @@ -280,7 +221,7 @@ setTimeout(async function () {
});
}

function notifyHotReloadApplied() {
function displayChangesAppliedToast() {
document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove());
if (document.querySelector('#dotnet-hotreload-toast')) {
return;
Expand All @@ -298,25 +239,47 @@ setTimeout(async function () {
setTimeout(() => el.remove(), 2000);
}

function aspnetCoreHotReloadApplied() {
function refreshBrowser() {
if (window.Blazor) {
window[hotReloadActiveKey] = true;
// hotReloadApplied triggers an enhanced navigation to
// refresh pages that have been statically rendered with
// Blazor SSR.
if (window.Blazor?._internal?.hotReloadApplied)
{
console.debug('Refreshing browser: WASM.');
Blazor._internal.hotReloadApplied();
}
else
{
notifyHotReloadApplied();
console.debug('Refreshing browser.');
displayChangesAppliedToast();
}
} else {
console.debug('Refreshing browser: Reloading.');
location.reload();
}
}

function reload() {
console.debug('Reloading.');
location.reload();
}

function wait() {
console.debug('Waiting for application to rebuild.');

if (waiting) {
return;
}

waiting = true;
const glyphs = ['☱', '☲', '☴'];
const title = document.title;
let i = 0;
setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240);
}

async function getSecret(serverKeyString) {
if (!serverKeyString || !window.crypto || !window.crypto.subtle) {
return null;
Expand Down Expand Up @@ -382,8 +345,8 @@ setTimeout(async function () {
webSocket.addEventListener('close', onClose);
if (window.Blazor?.removeEventListener && window.Blazor?.addEventListener)
{
webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', notifyHotReloadApplied));
window.Blazor?.addEventListener('enhancedload', notifyHotReloadApplied);
webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', displayChangesAppliedToast));
window.Blazor?.addEventListener('enhancedload', displayChangesAppliedToast);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ internal abstract class AbstractBrowserRefreshServer(string middlewareAssemblyPa
{
public const string ServerLogComponentName = "BrowserRefreshServer";

private static readonly ReadOnlyMemory<byte> s_reloadMessage = Encoding.UTF8.GetBytes("Reload");
private static readonly ReadOnlyMemory<byte> s_waitMessage = Encoding.UTF8.GetBytes("Wait");
private static readonly ReadOnlyMemory<byte> s_pingMessage = Encoding.UTF8.GetBytes("""{ "type" : "Ping" }""");
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);

private readonly List<BrowserConnection> _activeConnections = [];
Expand Down Expand Up @@ -233,19 +230,15 @@ public ValueTask SendJsonMessageAsync<TValue>(TValue value, CancellationToken ca
public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken)
{
logger.Log(LogEvents.ReloadingBrowser);
return SendAsync(s_reloadMessage, cancellationToken);
return SendAsync(JsonReloadRequest.Message, cancellationToken);
}

public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)
{
logger.Log(LogEvents.SendingWaitMessage);
return SendAsync(s_waitMessage, cancellationToken);
return SendAsync(JsonWaitRequest.Message, cancellationToken);
}

// obsolete: to be removed
public ValueTask SendPingMessageAsync(CancellationToken cancellationToken)
=> SendAsync(s_pingMessage, cancellationToken);

private ValueTask SendAsync(ReadOnlyMemory<byte> messageBytes, CancellationToken cancellationToken)
=> SendAndReceiveAsync(request: _ => messageBytes, response: null, cancellationToken);

Expand Down Expand Up @@ -293,13 +286,13 @@ public async ValueTask SendAndReceiveAsync<TRequest>(
public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken)
{
logger.Log(LogEvents.RefreshingBrowser);
return SendJsonMessageAsync(new AspNetCoreHotReloadApplied(), cancellationToken);
return SendAsync(JsonRefreshBrowserRequest.Message, cancellationToken);
}

public ValueTask ReportCompilationErrorsInBrowserAsync(ImmutableArray<string> compilationErrors, CancellationToken cancellationToken)
{
logger.Log(LogEvents.UpdatingDiagnostics);
return SendJsonMessageAsync(new HotReloadDiagnostics { Diagnostics = compilationErrors }, cancellationToken);
return SendJsonMessageAsync(new JsonReportDiagnosticsRequest { Diagnostics = compilationErrors }, cancellationToken);
}

public async ValueTask UpdateStaticAssetsAsync(IEnumerable<string> relativeUrls, CancellationToken cancellationToken)
Expand All @@ -308,24 +301,37 @@ public async ValueTask UpdateStaticAssetsAsync(IEnumerable<string> relativeUrls,
foreach (var relativeUrl in relativeUrls)
{
logger.Log(LogEvents.SendingStaticAssetUpdateRequest, relativeUrl);
var message = JsonSerializer.SerializeToUtf8Bytes(new UpdateStaticFileMessage { Path = relativeUrl }, s_jsonSerializerOptions);
var message = JsonSerializer.SerializeToUtf8Bytes(new JasonUpdateStaticFileRequest { Path = relativeUrl }, s_jsonSerializerOptions);
await SendAsync(message, cancellationToken);
}
}

private readonly struct AspNetCoreHotReloadApplied
private readonly struct JsonWaitRequest
{
public string Type => "Wait";
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonWaitRequest(), s_jsonSerializerOptions);
}

private readonly struct JsonReloadRequest
{
public string Type => "Reload";
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonReloadRequest(), s_jsonSerializerOptions);
}

private readonly struct JsonRefreshBrowserRequest
{
public string Type => "AspNetCoreHotReloadApplied";
public string Type => "RefreshBrowser";
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonRefreshBrowserRequest(), s_jsonSerializerOptions);
}

private readonly struct HotReloadDiagnostics
private readonly struct JsonReportDiagnosticsRequest
{
public string Type => "HotReloadDiagnosticsv1";
public string Type => "ReportDiagnostics";

public IEnumerable<string> Diagnostics { get; init; }
}

private readonly struct UpdateStaticFileMessage
private readonly struct JasonUpdateStaticFileRequest
{
public string Type => "UpdateStaticFile";
public string Path { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public override async Task<ApplyStatus> ApplyManagedCodeUpdatesAsync(ImmutableAr
var anyFailure = false;

await browserRefreshServer.SendAndReceiveAsync(
request: sharedSecret => new JsonApplyHotReloadDeltasRequest
request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest
{
SharedSecret = sharedSecret,
UpdateId = batchId,
Expand Down Expand Up @@ -178,9 +178,9 @@ private static bool ReceiveUpdateResponseAsync(ReadOnlySpan<byte> value, ILogger
public override Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;

private readonly struct JsonApplyHotReloadDeltasRequest
private readonly struct JsonApplyManagedCodeUpdatesRequest
{
public string Type => "BlazorHotReloadDeltav3";
public string Type => "ApplyManagedCodeUpdates";
public string? SharedSecret { get; init; }

public int UpdateId { get; init; }
Expand Down Expand Up @@ -211,7 +211,7 @@ private readonly struct JsonLogEntry

private readonly struct JsonGetApplyUpdateCapabilitiesRequest
{
public string Type => "BlazorRequestApplyUpdateCapabilities2";
public string Type => "GetApplyUpdateCapabilities";
}
}
}
12 changes: 6 additions & 6 deletions test/dotnet-watch.Tests/Browser/BrowserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public async Task BrowserDiagnostics()
await App.WaitForOutputLineContaining("Do you want to restart your app?");

await App.WaitUntilOutputContains($$"""
🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[{{jsonErrorMessage}}]}
🧪 Received: {"type":"ReportDiagnostics","diagnostics":[{{jsonErrorMessage}}]}
""");

// auto restart next time:
Expand All @@ -76,7 +76,7 @@ await App.WaitUntilOutputContains($$"""

// browser page was reloaded after the app restarted:
await App.WaitUntilOutputContains("""
🧪 Received: Reload
🧪 Received: {"type":"Reload"}
""");

// no other browser message sent:
Expand All @@ -93,14 +93,14 @@ await App.WaitUntilOutputContains("""
await App.WaitForOutputLineContaining("[auto-restart] " + errorMessage);

await App.WaitUntilOutputContains($$"""
🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":["Restarting application to apply changes ..."]}
🧪 Received: {"type":"ReportDiagnostics","diagnostics":["Restarting application to apply changes ..."]}
""");

await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges);

// browser page was reloaded after the app restarted:
await App.WaitUntilOutputContains("""
🧪 Received: Reload
🧪 Received: {"type":"Reload"}
""");

// no other browser message sent:
Expand All @@ -114,11 +114,11 @@ await App.WaitUntilOutputContains("""
await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded);

await App.WaitUntilOutputContains($$"""
🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[]}
🧪 Received: {"type":"ReportDiagnostics","diagnostics":[]}
""");

await App.WaitUntilOutputContains($$"""
🧪 Received: {"type":"AspNetCoreHotReloadApplied"}
🧪 Received: {"type":"RefreshBrowser"}
""");

// no other browser message sent:
Expand Down