Skip to content

Commit 43f8f05

Browse files
authored
Clean up browser communication protocol (#51226)
1 parent 78759c5 commit 43f8f05

File tree

4 files changed

+84
-115
lines changed

4 files changed

+84
-115
lines changed

src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js

Lines changed: 51 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -26,38 +26,22 @@ setTimeout(async function () {
2626

2727
let waiting = false;
2828

29-
connection.onmessage = function (message) {
30-
if (message.data === 'Reload') {
31-
console.debug('Server is ready. Reloading...');
32-
location.reload();
33-
} else if (message.data === 'Wait') {
34-
if (waiting) {
35-
return;
36-
}
37-
waiting = true;
38-
console.debug('File changes detected. Waiting for application to rebuild.');
39-
const glyphs = ['☱', '☲', '☴'];
40-
const title = document.title;
41-
let i = 0;
42-
setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240);
29+
connection.onmessage = function (message) {
30+
const payload = JSON.parse(message.data);
31+
const action = {
32+
'Reload': () => reload(),
33+
'Wait': () => wait(),
34+
'UpdateStaticFile': () => updateStaticFile(payload.path),
35+
'ApplyManagedCodeUpdates': () => applyManagedCodeUpdates(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel),
36+
'ReportDiagnostics': () => reportDiagnostics(payload.diagnostics),
37+
'GetApplyUpdateCapabilities': () => getApplyUpdateCapabilities(),
38+
'RefreshBrowser': () => refreshBrowser()
39+
};
40+
41+
if (payload.type && action.hasOwnProperty(payload.type)) {
42+
action[payload.type]();
4343
} else {
44-
const payload = JSON.parse(message.data);
45-
const action = {
46-
'UpdateStaticFile': () => updateStaticFile(payload.path),
47-
'BlazorHotReloadDeltav1': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, false),
48-
'BlazorHotReloadDeltav2': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, true),
49-
'BlazorHotReloadDeltav3': () => applyBlazorDeltas(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel),
50-
'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics),
51-
'BlazorRequestApplyUpdateCapabilities': () => getBlazorWasmApplyUpdateCapabilities(false),
52-
'BlazorRequestApplyUpdateCapabilities2': () => getBlazorWasmApplyUpdateCapabilities(true),
53-
'AspNetCoreHotReloadApplied': () => aspnetCoreHotReloadApplied()
54-
};
55-
56-
if (payload.type && action.hasOwnProperty(payload.type)) {
57-
action[payload.type]();
58-
} else {
59-
console.error('Unknown payload:', message.data);
60-
}
44+
console.error('Unknown payload:', message.data);
6145
}
6246
}
6347

@@ -106,12 +90,12 @@ setTimeout(async function () {
10690
return messageAndStack
10791
}
10892

109-
function getBlazorWasmApplyUpdateCapabilities(sendErrorToClient) {
93+
function getApplyUpdateCapabilities() {
11094
let applyUpdateCapabilities;
11195
try {
11296
applyUpdateCapabilities = window.Blazor._internal.getApplyUpdateCapabilities();
11397
} catch (error) {
114-
applyUpdateCapabilities = sendErrorToClient ? "!" + getMessageAndStack(error) : '';
98+
applyUpdateCapabilities = "!" + getMessageAndStack(error);
11599
}
116100
connection.send(applyUpdateCapabilities);
117101
}
@@ -137,41 +121,6 @@ setTimeout(async function () {
137121
styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling);
138122
}
139123

140-
async function applyBlazorDeltas_legacy(serverSecret, deltas, sendErrorToClient) {
141-
if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) {
142-
// Validate the shared secret if it was specified. It might be unspecified in older versions of VS
143-
// that do not support this feature as yet.
144-
throw 'Unable to validate the server. Rejecting apply-update payload.';
145-
}
146-
147-
let applyError = undefined;
148-
149-
try {
150-
applyDeltas_legacy(deltas)
151-
} catch (error) {
152-
console.warn(error);
153-
applyError = error;
154-
}
155-
156-
const body = JSON.stringify({
157-
id: deltas[0].sequenceId,
158-
deltas: deltas
159-
});
160-
try {
161-
await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body });
162-
} catch (error) {
163-
console.warn(error);
164-
applyError = error;
165-
}
166-
167-
if (applyError) {
168-
sendDeltaNotApplied(sendErrorToClient ? applyError : undefined);
169-
} else {
170-
sendDeltaApplied();
171-
notifyHotReloadApplied();
172-
}
173-
}
174-
175124
function applyDeltas_legacy(deltas) {
176125
let apply = window.Blazor?._internal?.applyHotReload
177126

@@ -190,26 +139,16 @@ setTimeout(async function () {
190139
});
191140
}
192141
}
193-
function sendDeltaApplied() {
194-
connection.send(new Uint8Array([1]).buffer);
195-
}
196-
197-
function sendDeltaNotApplied(error) {
198-
if (error) {
199-
let encoder = new TextEncoder()
200-
connection.send(encoder.encode("\0" + error.message + "\0" + error.stack));
201-
} else {
202-
connection.send(new Uint8Array([0]).buffer);
203-
}
204-
}
205142

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

150+
console.debug('Applying managed code updates.');
151+
213152
const AgentMessageSeverity_Error = 2
214153

215154
let applyError = undefined;
@@ -261,11 +200,13 @@ setTimeout(async function () {
261200
}));
262201

263202
if (!applyError) {
264-
notifyHotReloadApplied();
203+
displayChangesAppliedToast();
265204
}
266205
}
267206

268-
function displayDiagnostics(diagnostics) {
207+
function reportDiagnostics(diagnostics) {
208+
console.debug('Reporting Hot Reload diagnostics.');
209+
269210
document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove());
270211
const el = document.body.appendChild(document.createElement('div'));
271212
el.id = 'dotnet-compile-error';
@@ -280,7 +221,7 @@ setTimeout(async function () {
280221
});
281222
}
282223

283-
function notifyHotReloadApplied() {
224+
function displayChangesAppliedToast() {
284225
document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove());
285226
if (document.querySelector('#dotnet-hotreload-toast')) {
286227
return;
@@ -298,25 +239,47 @@ setTimeout(async function () {
298239
setTimeout(() => el.remove(), 2000);
299240
}
300241

301-
function aspnetCoreHotReloadApplied() {
242+
function refreshBrowser() {
302243
if (window.Blazor) {
303244
window[hotReloadActiveKey] = true;
304245
// hotReloadApplied triggers an enhanced navigation to
305246
// refresh pages that have been statically rendered with
306247
// Blazor SSR.
307248
if (window.Blazor?._internal?.hotReloadApplied)
308249
{
250+
console.debug('Refreshing browser: WASM.');
309251
Blazor._internal.hotReloadApplied();
310252
}
311253
else
312254
{
313-
notifyHotReloadApplied();
255+
console.debug('Refreshing browser.');
256+
displayChangesAppliedToast();
314257
}
315258
} else {
259+
console.debug('Refreshing browser: Reloading.');
316260
location.reload();
317261
}
318262
}
319263

264+
function reload() {
265+
console.debug('Reloading.');
266+
location.reload();
267+
}
268+
269+
function wait() {
270+
console.debug('Waiting for application to rebuild.');
271+
272+
if (waiting) {
273+
return;
274+
}
275+
276+
waiting = true;
277+
const glyphs = ['☱', '☲', '☴'];
278+
const title = document.title;
279+
let i = 0;
280+
setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240);
281+
}
282+
320283
async function getSecret(serverKeyString) {
321284
if (!serverKeyString || !window.crypto || !window.crypto.subtle) {
322285
return null;
@@ -382,8 +345,8 @@ setTimeout(async function () {
382345
webSocket.addEventListener('close', onClose);
383346
if (window.Blazor?.removeEventListener && window.Blazor?.addEventListener)
384347
{
385-
webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', notifyHotReloadApplied));
386-
window.Blazor?.addEventListener('enhancedload', notifyHotReloadApplied);
348+
webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', displayChangesAppliedToast));
349+
window.Blazor?.addEventListener('enhancedload', displayChangesAppliedToast);
387350
}
388351
});
389352
}

src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ internal abstract class AbstractBrowserRefreshServer(string middlewareAssemblyPa
2929
{
3030
public const string ServerLogComponentName = "BrowserRefreshServer";
3131

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

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

239236
public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)
240237
{
241238
logger.Log(LogEvents.SendingWaitMessage);
242-
return SendAsync(s_waitMessage, cancellationToken);
239+
return SendAsync(JsonWaitRequest.Message, cancellationToken);
243240
}
244241

245-
// obsolete: to be removed
246-
public ValueTask SendPingMessageAsync(CancellationToken cancellationToken)
247-
=> SendAsync(s_pingMessage, cancellationToken);
248-
249242
private ValueTask SendAsync(ReadOnlyMemory<byte> messageBytes, CancellationToken cancellationToken)
250243
=> SendAndReceiveAsync(request: _ => messageBytes, response: null, cancellationToken);
251244

@@ -293,13 +286,13 @@ public async ValueTask SendAndReceiveAsync<TRequest>(
293286
public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken)
294287
{
295288
logger.Log(LogEvents.RefreshingBrowser);
296-
return SendJsonMessageAsync(new AspNetCoreHotReloadApplied(), cancellationToken);
289+
return SendAsync(JsonRefreshBrowserRequest.Message, cancellationToken);
297290
}
298291

299292
public ValueTask ReportCompilationErrorsInBrowserAsync(ImmutableArray<string> compilationErrors, CancellationToken cancellationToken)
300293
{
301294
logger.Log(LogEvents.UpdatingDiagnostics);
302-
return SendJsonMessageAsync(new HotReloadDiagnostics { Diagnostics = compilationErrors }, cancellationToken);
295+
return SendJsonMessageAsync(new JsonReportDiagnosticsRequest { Diagnostics = compilationErrors }, cancellationToken);
303296
}
304297

305298
public async ValueTask UpdateStaticAssetsAsync(IEnumerable<string> relativeUrls, CancellationToken cancellationToken)
@@ -308,24 +301,37 @@ public async ValueTask UpdateStaticAssetsAsync(IEnumerable<string> relativeUrls,
308301
foreach (var relativeUrl in relativeUrls)
309302
{
310303
logger.Log(LogEvents.SendingStaticAssetUpdateRequest, relativeUrl);
311-
var message = JsonSerializer.SerializeToUtf8Bytes(new UpdateStaticFileMessage { Path = relativeUrl }, s_jsonSerializerOptions);
304+
var message = JsonSerializer.SerializeToUtf8Bytes(new JasonUpdateStaticFileRequest { Path = relativeUrl }, s_jsonSerializerOptions);
312305
await SendAsync(message, cancellationToken);
313306
}
314307
}
315308

316-
private readonly struct AspNetCoreHotReloadApplied
309+
private readonly struct JsonWaitRequest
310+
{
311+
public string Type => "Wait";
312+
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonWaitRequest(), s_jsonSerializerOptions);
313+
}
314+
315+
private readonly struct JsonReloadRequest
316+
{
317+
public string Type => "Reload";
318+
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonReloadRequest(), s_jsonSerializerOptions);
319+
}
320+
321+
private readonly struct JsonRefreshBrowserRequest
317322
{
318-
public string Type => "AspNetCoreHotReloadApplied";
323+
public string Type => "RefreshBrowser";
324+
public static readonly ReadOnlyMemory<byte> Message = JsonSerializer.SerializeToUtf8Bytes(new JsonRefreshBrowserRequest(), s_jsonSerializerOptions);
319325
}
320326

321-
private readonly struct HotReloadDiagnostics
327+
private readonly struct JsonReportDiagnosticsRequest
322328
{
323-
public string Type => "HotReloadDiagnosticsv1";
329+
public string Type => "ReportDiagnostics";
324330

325331
public IEnumerable<string> Diagnostics { get; init; }
326332
}
327333

328-
private readonly struct UpdateStaticFileMessage
334+
private readonly struct JasonUpdateStaticFileRequest
329335
{
330336
public string Type => "UpdateStaticFile";
331337
public string Path { get; init; }

src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public override async Task<ApplyStatus> ApplyManagedCodeUpdatesAsync(ImmutableAr
133133
var anyFailure = false;
134134

135135
await browserRefreshServer.SendAndReceiveAsync(
136-
request: sharedSecret => new JsonApplyHotReloadDeltasRequest
136+
request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest
137137
{
138138
SharedSecret = sharedSecret,
139139
UpdateId = batchId,
@@ -178,9 +178,9 @@ private static bool ReceiveUpdateResponseAsync(ReadOnlySpan<byte> value, ILogger
178178
public override Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
179179
=> Task.CompletedTask;
180180

181-
private readonly struct JsonApplyHotReloadDeltasRequest
181+
private readonly struct JsonApplyManagedCodeUpdatesRequest
182182
{
183-
public string Type => "BlazorHotReloadDeltav3";
183+
public string Type => "ApplyManagedCodeUpdates";
184184
public string? SharedSecret { get; init; }
185185

186186
public int UpdateId { get; init; }
@@ -211,7 +211,7 @@ private readonly struct JsonLogEntry
211211

212212
private readonly struct JsonGetApplyUpdateCapabilitiesRequest
213213
{
214-
public string Type => "BlazorRequestApplyUpdateCapabilities2";
214+
public string Type => "GetApplyUpdateCapabilities";
215215
}
216216
}
217217
}

test/dotnet-watch.Tests/Browser/BrowserTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public async Task BrowserDiagnostics()
6565
await App.WaitForOutputLineContaining("Do you want to restart your app?");
6666

6767
await App.WaitUntilOutputContains($$"""
68-
🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[{{jsonErrorMessage}}]}
68+
🧪 Received: {"type":"ReportDiagnostics","diagnostics":[{{jsonErrorMessage}}]}
6969
""");
7070

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

7777
// browser page was reloaded after the app restarted:
7878
await App.WaitUntilOutputContains("""
79-
🧪 Received: Reload
79+
🧪 Received: {"type":"Reload"}
8080
""");
8181

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

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

9999
await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges);
100100

101101
// browser page was reloaded after the app restarted:
102102
await App.WaitUntilOutputContains("""
103-
🧪 Received: Reload
103+
🧪 Received: {"type":"Reload"}
104104
""");
105105

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

116116
await App.WaitUntilOutputContains($$"""
117-
🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[]}
117+
🧪 Received: {"type":"ReportDiagnostics","diagnostics":[]}
118118
""");
119119

120120
await App.WaitUntilOutputContains($$"""
121-
🧪 Received: {"type":"AspNetCoreHotReloadApplied"}
121+
🧪 Received: {"type":"RefreshBrowser"}
122122
""");
123123

124124
// no other browser message sent:

0 commit comments

Comments
 (0)