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
18 changes: 14 additions & 4 deletions src/Components/Server.AutoPause/src/js/autopause.lib.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,30 @@ interface WebStartOptionsLike {
circuit?: Record<string, unknown>;
}

type ServerStartOptionsLike = Record<string, unknown>;

let config: AutoPauseConfig | undefined;
let manager: AutoPauseManager | undefined;

function beforeWebStart(options: WebStartOptionsLike): void {
const enabled = options.circuit?.['autoPauseEnabled'] as boolean | undefined;
function configure(options: Record<string, unknown> | undefined): void {
const enabled = options?.['autoPauseEnabled'] as boolean | undefined;
if (enabled === undefined) {
return;
}
config = {
enabled,
hiddenDelayMilliseconds: options.circuit?.['autoPauseHiddenDelayMilliseconds'] as number | undefined ?? 120000,
hiddenDelayMilliseconds: options?.['autoPauseHiddenDelayMilliseconds'] as number | undefined ?? 120000,
};
}

function beforeWebStart(options: WebStartOptionsLike): void {
configure(options.circuit);
}

function beforeServerStart(options: ServerStartOptionsLike): void {
configure(options);
}

// Called by the framework once Blazor has started; activates auto-pause when AddAutoPause
// enabled it. A second call disposes the previous manager so listeners never accumulate.
function afterWebStarted(blazor: BlazorLike): void {
Expand All @@ -41,4 +51,4 @@ function afterWebStarted(blazor: BlazorLike): void {
mgr.start();
}

export { beforeWebStart, beforeWebStart as beforeServerStart, afterWebStarted, afterWebStarted as afterServerStarted };
export { beforeWebStart, beforeServerStart, afterWebStarted, afterWebStarted as afterServerStarted };
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,16 @@ describe('autopause initializer', () => {
const attached = blazor.added.filter(h => !blazor.removed.includes(h));
expect(attached).toHaveLength(1);
});

it('server start reads auto-pause configuration discovered during enhanced navigation', () => {
const blazor = createBlazor();

beforeWebStart({ circuit: { autoPauseEnabled: false } });
afterWebStarted(blazor);
expect(blazor.added).toHaveLength(0);

beforeServerStart({ autoPauseEnabled: true, autoPauseHiddenDelayMilliseconds: 100 });
afterServerStarted(blazor);
expect(blazor.added).toHaveLength(1);
});
});
15 changes: 5 additions & 10 deletions src/Components/Web.JS/src/Boot.Server.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,20 @@ import { RootComponentManager } from './Services/RootComponentManager';
import { WebRendererId } from './Rendering/WebRendererId';
import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods';

let initializersPromise: Promise<void> | undefined;
let circuitOptionsPromise: Promise<Partial<CircuitStartOptions>> | undefined;
let appState: string;
let circuit: CircuitManager;
let options: CircuitStartOptions;
let logger: ConsoleLogger;
let serverStartPromise: Promise<void>;
let circuitStarting: Promise<boolean> | undefined;

export function setCircuitOptions(initializersReady: Promise<Partial<CircuitStartOptions>>) {
if (options) {
export function setCircuitOptions(optionsReady: Promise<Partial<CircuitStartOptions>>) {
if (circuitOptionsPromise) {
throw new Error('Circuit options have already been configured.');
}

initializersPromise = setOptions(initializersReady);

async function setOptions(initializers: Promise<Partial<CircuitStartOptions>>): Promise<void> {
const configuredOptions = await initializers;
options = resolveOptions(configuredOptions);
}
circuitOptionsPromise = optionsReady;
}

export function startServer(components: RootComponentManager<ServerComponentDescriptor>, jsEventRegistry: JSEventRegistry): Promise<void> {
Expand All @@ -46,7 +41,7 @@ export function startServer(components: RootComponentManager<ServerComponentDesc
}

async function startServerCore(components: RootComponentManager<ServerComponentDescriptor>, jsEventRegistry: JSEventRegistry, resolve: () => void, _: any) {
await initializersPromise;
options = resolveOptions(await circuitOptionsPromise);
const jsInitializer = await fetchAndInvokeInitializers(options);

appState = discoverServerPersistedState(document) || '';
Expand Down
67 changes: 36 additions & 31 deletions src/Components/Web.JS/src/Boot.Web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
enhancedNavigationStarted: () => {
jsEventRegistry.dispatchEvent('enhancednavigationstart', {});
},
beforeDomUpdate: (source) => {
updateOptionsFromBrowserConfiguration(options, source);
},
documentUpdated: () => {
rootComponentManager.onDocumentUpdated();
resetScrollIfNeeded(ScrollResetSchedule.AfterDocumentUpdate);
Expand Down Expand Up @@ -99,8 +102,31 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
}

function onInitialDomContentLoaded(options: Partial<WebStartOptions>) {
// Discover server-emitted browser configuration and merge into options
const browserConfig = discoverBrowserConfiguration(document);
updateOptionsFromBrowserConfiguration(options);

// Retrieve and start invoking the initializers.
// Blazor server options get defaults that are configured before we invoke the initializers
// so we do the same here.
const initialCircuitOptions = resolveOptions(options?.circuit || {});
options.circuit = initialCircuitOptions;
options.webAssembly = options.webAssembly || ({} as WebAssemblyStartOptions);
const logger = new ConsoleLogger(initialCircuitOptions.logLevel);
const initializersPromise = fetchAndInvokeInitializers(options, logger);
setCircuitOptions(resolveConfiguredOptions(initializersPromise, initialCircuitOptions));
setWebAssemblyOptions(resolveConfiguredOptions(initializersPromise, options.webAssembly));

registerAllComponentDescriptors(document);

rootComponentManager.onDocumentUpdated();

// Initialize client-side validation if the page has validatable fields.
initFormValidationIfNeeded();

callAfterStartedCallbacks(initializersPromise);
}

function updateOptionsFromBrowserConfiguration(options: Partial<WebStartOptions>, source: Node = document): void {
const browserConfig = discoverBrowserConfiguration(source);
if (browserConfig) {
if (browserConfig.logLevel !== undefined) {
options.logLevel = browserConfig.logLevel;
Expand Down Expand Up @@ -141,37 +167,16 @@ function onInitialDomContentLoaded(options: Partial<WebStartOptions>) {
}
}
}
}

// Retrieve and start invoking the initializers.
// Blazor server options get defaults that are configured before we invoke the initializers
// so we do the same here.
const initialCircuitOptions = resolveOptions(options?.circuit || {});
options.circuit = initialCircuitOptions;
options.webAssembly = options.webAssembly || ({} as WebAssemblyStartOptions);
const logger = new ConsoleLogger(initialCircuitOptions.logLevel);
const initializersPromise = fetchAndInvokeInitializers(options, logger);
setCircuitOptions(resolveConfiguredOptions(initializersPromise, initialCircuitOptions));
setWebAssemblyOptions(resolveConfiguredOptions(initializersPromise, options.webAssembly));

// If BrowserConfiguration had WebAssembly server options, apply them
// before registering component descriptors, since registration triggers
// WebAssembly platform loading which captures these options.
if (browserConfig?.webAssembly) {
rootComponentManager.setWebAssemblyOptions({
environmentName: browserConfig.webAssembly.environmentName ?? '',
environmentVariables: browserConfig.webAssembly.environmentVariables ?? {},
});
// Apply WebAssembly server options before processing component descriptors, since
// registration can trigger platform loading that captures these options.
if (browserConfig.webAssembly) {
rootComponentManager.setWebAssemblyOptions({
environmentName: browserConfig.webAssembly.environmentName ?? '',
environmentVariables: browserConfig.webAssembly.environmentVariables ?? {},
});
}
}

registerAllComponentDescriptors(document);

rootComponentManager.onDocumentUpdated();

// Initialize client-side validation if the page has validatable fields.
initFormValidationIfNeeded();

callAfterStartedCallbacks(initializersPromise);
}

function initFormValidationIfNeeded(): void {
Expand Down
3 changes: 3 additions & 0 deletions src/Components/Web.JS/src/Rendering/StreamingRendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ function redirect(node: HTMLTemplateElement, changeUrl: boolean, isEnhancedNav:
}

function insertStreamingContentIntoDocument(componentIdAsString: string, docFrag: DocumentFragment): void {
// Apply configuration before streamed components can activate.
navigationEnhancementCallbacks.beforeDomUpdate(docFrag);

const markers = findStreamingMarkers(componentIdAsString);
if (markers) {
const { startMarker, endMarker } = markers;
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Web.JS/src/Services/NavigationEnhancement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let currentContentUrl = location.href;

export interface NavigationEnhancementCallbacks {
enhancedNavigationStarted: () => void;
beforeDomUpdate: (source: Node) => void;
documentUpdated: () => void;
enhancedNavigationCompleted: () => void;
}
Expand Down Expand Up @@ -309,6 +310,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
if (responseContentType?.startsWith('text/html') && initialContent) {
// For HTML responses, regardless of the status code, display it
const parsedHtml = new DOMParser().parseFromString(initialContent, 'text/html');
navigationEnhancementCallbacks.beforeDomUpdate(parsedHtml);
synchronizeDomContent(document, parsedHtml);
navigationEnhancementCallbacks.documentUpdated();
} else if (responseContentType?.startsWith('text/') && initialContent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Components.TestServer.RazorComponents;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;

public class AutoPauseEnhancedNavigationTests : AutoPauseTestBase<App>
{
public AutoPauseEnhancedNavigationTests(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
serverFixture.AdditionalArguments.AddRange("--DisableReconnectionCache", "true");
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void AutoPause_StartsWhenInteractiveServerStartsAfterEnhancedNavigation(bool streaming)
{
Navigate("/subdir/persistent-state/auto-pause-enhanced-navigation-landing");
var htmlElement = Browser.Exists(By.TagName("html"));

var linkId = streaming ? "navigate-to-streaming-auto-pause" : "navigate-to-auto-pause";
Browser.Exists(By.Id(linkId)).Click();
Browser.Exists(By.Id("increment-persistent-counter-count"));
Browser.False(() => htmlElement.IsStale());

Browser.Exists(By.Id("increment-persistent-counter-count")).Click();
Browser.Equal("1", () => Browser.Exists(By.Id("persistent-counter-count")).Text);
Browser.Exists(By.Id("increment-non-persisted-counter")).Click();
Browser.Equal("6", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

SetVisibility("hidden");
WaitForPausedUI();

SetVisibility("visible");
WaitForResumedUI();

Browser.Equal("1", () => Browser.Exists(By.Id("persistent-counter-count")).Text);
Browser.Equal("0", () => Browser.Exists(By.Id("non-persisted-counter")).Text);
Browser.Exists(By.Id("increment-persistent-counter-count")).Click();
Browser.Equal("2", () => Browser.Exists(By.Id("persistent-counter-count")).Text);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;

public abstract class AutoPauseTestBase<TRootComponent>
: ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<TRootComponent>>>
{
protected AutoPauseTestBase(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<TRootComponent>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}

protected void SetVisibility(string state)
{
var js = (IJavaScriptExecutor)Browser;
js.ExecuteScript($@"
Object.defineProperty(document, 'visibilityState', {{ configurable: true, get: () => '{state}' }});
Object.defineProperty(document, 'hidden', {{ configurable: true, get: () => {(state == "hidden" ? "true" : "false")} }});
document.dispatchEvent(new Event('visibilitychange'));
");
}

protected void WaitForPausedUI()
{
Browser.Equal("block", () =>
Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display"));
}

protected void WaitForResumedUI()
{
Browser.Equal("none", () =>
Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;

public class AutoPauseTests : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<Root>>>
public class AutoPauseTests : AutoPauseTestBase<Root>
{
public AutoPauseTests(
BrowserFixture browserFixture,
Expand Down Expand Up @@ -104,16 +104,6 @@ public void HiddenTab_BecomesVisibleBeforeDelay_DoesNotPause()
Assert.Empty(GetAutoPauseEvents());
}

private void SetVisibility(string state)
{
var js = (IJavaScriptExecutor)Browser;
js.ExecuteScript($@"
Object.defineProperty(document, 'visibilityState', {{ configurable: true, get: () => '{state}' }});
Object.defineProperty(document, 'hidden', {{ configurable: true, get: () => {(state == "hidden" ? "true" : "false")} }});
document.dispatchEvent(new Event('visibilitychange'));
");
}

private IReadOnlyList<string> GetAutoPauseEvents()
{
var js = (IJavaScriptExecutor)Browser;
Expand All @@ -132,15 +122,4 @@ private void WaitForBlazorPause()
"return !!(window.Blazor && window.myApp && typeof window.myApp.deferPause === 'function')"));
}

private void WaitForPausedUI()
{
Browser.Equal("block", () =>
Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display"));
}

private void WaitForResumedUI()
{
Browser.Equal("none", () =>
Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,38 @@ public void InitializersRunsClassicInitializersWhenEnabled(bool streaming, bool
}
}

[Fact]
public void ServerInitializerActivatedAfterEnhancedNavigationFromStaticPage()
{
Navigate($"{ServerPathBase}/initializers?streaming=false&wasm=false&server=false&auto-pause=true&auto-pause-delay-ms=10");

Browser.True(() => (bool)((IJavaScriptExecutor)Browser).ExecuteScript(
"return typeof Blazor.pauseCircuit === 'undefined'"));

((IJavaScriptExecutor)Browser).ExecuteScript(
"Blazor.navigateTo('persistent-state/server-pause?auto-pause=true&auto-pause-delay-ms=10')");

Browser.Exists(By.Id("render-mode-interactive"));
Browser.True(() => (bool)((IJavaScriptExecutor)Browser).ExecuteScript(
"return typeof Blazor.pauseCircuit === 'function'"));

((IJavaScriptExecutor)Browser).ExecuteScript(
"""
window.autoPauseCallCount = 0;
const pauseCircuit = Blazor.pauseCircuit;
Blazor.pauseCircuit = (...args) => {
window.autoPauseCallCount++;
return pauseCircuit(...args);
};
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });
Object.defineProperty(document, 'hidden', { configurable: true, get: () => true });
document.dispatchEvent(new Event('visibilitychange'));
""");

Browser.Equal(1L, () => (long)((IJavaScriptExecutor)Browser).ExecuteScript(
"return window.autoPauseCallCount"));
}

private void EnableClassicInitializers(IWebDriver browser)
{
browser.Navigate().GoToUrl($"{new Uri(_serverFixture.RootUri, ServerPathBase)}/");
Expand Down
Loading
Loading