Files
infocom-systems-design/node_modules/chromium-bidi/lib/esm/bidiMapper/modules/context/NavigationTracker.js
2025-10-03 22:27:28 +03:00

318 lines
13 KiB
JavaScript

/*
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { ChromiumBidi, } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { getTimestamp } from '../../../utils/time.js';
import { urlMatchesAboutBlank } from '../../../utils/UrlHelpers.js';
import { uuidv4 } from '../../../utils/uuid.js';
export class NavigationResult {
eventName;
message;
constructor(eventName, message) {
this.eventName = eventName;
this.message = message;
}
}
class NavigationState {
navigationId = uuidv4();
#browsingContextId;
#started = false;
#finished = new Deferred();
url;
loaderId;
#isInitial;
#eventManager;
#navigated = false;
get finished() {
return this.#finished;
}
constructor(url, browsingContextId, isInitial, eventManager) {
this.#browsingContextId = browsingContextId;
this.url = url;
this.#isInitial = isInitial;
this.#eventManager = eventManager;
}
navigationInfo() {
return {
context: this.#browsingContextId,
navigation: this.navigationId,
timestamp: getTimestamp(),
url: this.url,
};
}
start() {
if (!this.#isInitial && !this.#started) {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#started = true;
}
#finish(navigationResult) {
this.#started = true;
if (!this.#isInitial &&
!this.#finished.isFinished &&
navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
this.#eventManager.registerEvent({
type: 'event',
method: navigationResult.eventName,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#finished.resolve(navigationResult);
}
frameNavigated() {
this.#navigated = true;
}
fragmentNavigated() {
this.#navigated = true;
this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
}
load() {
this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
}
fail(message) {
this.#finish(new NavigationResult(this.#navigated
? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
: "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
}
}
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
export class NavigationTracker {
#eventManager;
#logger;
#loaderIdToNavigationsMap = new Map();
#browsingContextId;
#currentNavigation;
// When a new navigation is started via `BrowsingContext.navigate` with `wait` set to
// `None`, the command result should have `navigation` value, but mapper does not have
// it yet. This value will be set to `navigationId` after next .
#pendingNavigation;
// Flags if the initial navigation to `about:blank` is in progress.
#isInitialNavigation = true;
navigation = {
withinDocument: new Deferred(),
};
constructor(url, browsingContextId, eventManager, logger) {
this.#browsingContextId = browsingContextId;
this.#eventManager = eventManager;
this.#logger = logger;
this.#isInitialNavigation = true;
this.#currentNavigation = new NavigationState(url, browsingContextId, urlMatchesAboutBlank(url), this.#eventManager);
}
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId() {
if (this.#pendingNavigation?.loaderId !== undefined) {
return this.#pendingNavigation.navigationId;
}
return this.#currentNavigation.navigationId;
}
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation() {
return this.#isInitialNavigation;
}
/**
* Url of the last navigated navigation.
*/
get url() {
return this.#currentNavigation.url;
}
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url, canBeInitialNavigation = false) {
this.#logger?.(LogType.debug, 'createCommandNavigation');
this.#isInitialNavigation =
canBeInitialNavigation &&
this.#isInitialNavigation &&
urlMatchesAboutBlank(url);
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
this.#pendingNavigation = navigation;
return navigation;
}
dispose() {
this.#pendingNavigation?.fail('navigation canceled by context disposal');
this.#currentNavigation.fail('navigation canceled by context disposal');
}
// Update the current url.
onTargetInfoChanged(url) {
this.#logger?.(LogType.debug, `onTargetInfoChanged ${url}`);
this.#currentNavigation.url = url;
}
#getNavigationForFrameNavigated(url, loaderId) {
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
return this.#loaderIdToNavigationsMap.get(loaderId);
}
if (this.#pendingNavigation !== undefined &&
this.#pendingNavigation?.loaderId === undefined) {
// This can be a pending navigation to `about:blank` created by a command. Use the
// pending navigation in this case.
return this.#pendingNavigation;
}
// Create a new pending navigation.
return this.createPendingNavigation(url, true);
}
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url, loaderId, unreachableUrl) {
this.#logger?.(LogType.debug, `frameNavigated ${url}`);
if (unreachableUrl !== undefined &&
!this.#loaderIdToNavigationsMap.has(loaderId)) {
// The navigation failed before started. Get or create pending navigation and fail
// it.
const navigation = this.#pendingNavigation ??
this.createPendingNavigation(unreachableUrl, true);
navigation.url = unreachableUrl;
navigation.start();
navigation.fail('the requested url is unreachable');
return;
}
const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
navigation.frameNavigated();
if (navigation !== this.#currentNavigation) {
this.#currentNavigation.fail('navigation canceled by concurrent navigation');
}
navigation.url = url;
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
navigation.start();
this.#currentNavigation = navigation;
if (this.#pendingNavigation === navigation) {
this.#pendingNavigation = undefined;
}
}
navigatedWithinDocument(url, navigationType) {
this.#logger?.(LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
// Current navigation URL should be updated.
this.#currentNavigation.url = url;
if (navigationType !== 'fragment') {
// TODO: check for other navigation types, like `javascript`.
return;
}
// There is no way to guaranteed match pending navigation with finished fragment
// navigations. So assume any pending navigation without loader id is the fragment
// one.
const fragmentNavigation = this.#pendingNavigation !== undefined &&
this.#pendingNavigation.loaderId === undefined
? this.#pendingNavigation
: new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
// Finish ongoing navigation.
fragmentNavigation.fragmentNavigated();
if (fragmentNavigation === this.#pendingNavigation) {
this.#pendingNavigation = undefined;
}
}
frameRequestedNavigation(url) {
this.#logger?.(LogType.debug, `Page.frameRequestedNavigation ${url}`);
// The page is about to navigate to the url.
this.createPendingNavigation(url, true);
}
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId) {
this.#logger?.(LogType.debug, 'loadPageEvent');
// Even if it was an initial navigation, it is finished.
this.#isInitialNavigation = false;
this.#loaderIdToNavigationsMap.get(loaderId)?.load();
}
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation, errorText) {
this.#logger?.(LogType.debug, 'failCommandNavigation');
navigation.fail(errorText);
}
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation, loaderId) {
this.#logger?.(LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
if (loaderId !== undefined) {
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
}
if (loaderId === undefined || this.#currentNavigation === navigation) {
// If the command's navigation is same-document or is already the current one,
// nothing to do.
return;
}
this.#currentNavigation.fail('navigation canceled by concurrent navigation');
navigation.start();
this.#currentNavigation = navigation;
if (this.#pendingNavigation === navigation) {
this.#pendingNavigation = undefined;
}
}
/**
* Emulated event, tight to `Network.requestWillBeSent`.
*/
frameStartedNavigating(url, loaderId) {
this.#logger?.(LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
// The `frameStartedNavigating` is tight to the `Network.requestWillBeSent` event
// which can be emitted several times, e.g. in case of redirection. Nothing to do in
// such a case.
return;
}
const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
pendingNavigation.url = url;
pendingNavigation.start();
pendingNavigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
}
/**
* In case of `beforeunload` handler, the pending navigation should be marked as started
* for consistency, as the `browsingContext.navigationStarted` should be emitted before
* user prompt.
*/
beforeunload() {
this.#logger?.(LogType.debug, `beforeunload`);
if (this.#pendingNavigation === undefined) {
this.#logger?.(LogType.debugError, `Unexpectedly no pending navigation on beforeunload`);
return;
}
this.#pendingNavigation.start();
}
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId, errorText) {
this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
}
}
//# sourceMappingURL=NavigationTracker.js.map