App History API

Draft Community Group Report,

This version:
https://wicg.github.io/app-history/
Editor:
Domenic Denicola (Google)
Participate:
GitHub WICG/app-history (new issue, open issues)
Commits:
GitHub spec.bs commits

Abstract

The app history API provides a web application-focused way of managing same-origin same-frame history entries and navigations.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. The AppHistory class

partial interface Window {
  readonly attribute AppHistory appHistory;
};

Each Window object has an associated app history, which is a new AppHistory instance created alongside the Window.

The appHistory getter steps are to return this's app history.

[Exposed=Window]
interface AppHistory : EventTarget {
  sequence<AppHistoryEntry> entries();
  readonly attribute AppHistoryEntry? current;
  undefined updateCurrent(AppHistoryUpdateCurrentOptions options);
  readonly attribute AppHistoryTransition? transition;

  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;

  AppHistoryResult navigate(USVString url, optional AppHistoryNavigateOptions options = {});
  AppHistoryResult reload(optional AppHistoryReloadOptions options = {});

  AppHistoryResult goTo(DOMString key, optional AppHistoryNavigationOptions options = {});
  AppHistoryResult back(optional AppHistoryNavigationOptions options = {});
  AppHistoryResult forward(optional AppHistoryNavigationOptions options = {});

  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
};

dictionary AppHistoryUpdateCurrentOptions {
  required any state;
};

dictionary AppHistoryNavigationOptions {
  any info;
};

dictionary AppHistoryNavigateOptions : AppHistoryNavigationOptions {
  any state;
  boolean replace = false;
};

dictionary AppHistoryReloadOptions : AppHistoryNavigationOptions {
  any state;
};

dictionary AppHistoryResult {
  Promise<AppHistoryEntry> committed;
  Promise<AppHistoryEntry> finished;
};

Each AppHistory object has an associated entry list, a list of AppHistoryEntry objects, initially empty.

Each AppHistory object has an associated current index, an integer, initially −1.

An AppHistory appHistory has entries and events disabled if the following steps return true:
  1. Let browsingContext be appHistory’s relevant global object's browsing context.

  2. If browsingContext is null, then return true.

  3. If browsingContext is still on its initial about:blank Document, then return true.

  4. If appHistory’s relevant global object's associated Document's origin is opaque, then return true.

  5. Return false.

To update the entries for an AppHistory instance appHistory:
  1. If appHistory has entries and events disabled, then:

    1. Assert: appHistory’s entry list is empty.

    2. Return.

  2. Let sessionHistory be appHistory’s relevant global object's browsing context's session history.

  3. Let appHistorySHEs be a new empty list.

  4. Let currentSHE be sessionHistory’s current entry.

  5. Let backwardIndex be the index of currentSHE within sessionHistory, minus 1.

  6. While backwardIndex > 0:

    1. Let she be sessionHistory[backwardIndex].

    2. If she’s origin is same origin with currentSHE’s origin, then prepend she to appHistorySHEs.

    3. Otherwise, break.

    4. Set backwardIndex to backwardIndex − 1.

  7. Append currentSHE to appHistorySHEs.

  8. Let forwardIndex be the index of currentSHE within sessionHistory, plus 1.

  9. While forwardIndex < sessionHistory’s size:

    1. Let she be sessionHistory[forwardIndex].

    2. If she’s origin is same origin with currentSHE’s origin, then append she to appHistorySHEs.

    3. Otherwise, break.

    4. Set forwardIndex to forwardIndex + 1.

  10. Let newCurrentIndex be the index of currentSHE within appHistorySHEs.

  11. Let newEntryList be an empty list.

  12. For each oldAHE of appHistory’s entry list:

    1. Set oldAHE’s index to −1.

  13. Let index be 0.

  14. Let disposedAHEs be a clone of appHistory’s entry list.

  15. For each she of appHistorySHEs:

    1. If appHistory’s entry list contains an AppHistoryEntry existingAHE whose session history entry is she, then:

      1. Append existingAHE to newEntryList.

      2. Remove existingAHE from disposedAHEs.

    2. Otherwise:

      1. Let newAHE be a new AppHistoryEntry created in the relevant realm of appHistory.

      2. Set newAHE’s session history entry to she.

      3. Append newAHE to newEntryList.

    3. Set newEntryList[index]'s index to index.

    4. Set index to index + 1.

  16. Set appHistory’s entry list to newEntryList.

  17. Set appHistory’s current index to newCurrentIndex.

  18. If appHistory’s ongoing navigation is non-null, then notify about the committed-to entry given appHistory’s ongoing navigation and the current entry of appHistory.

    It is important to do this before firing the dispose events, since event handlers for dispose could start another navigation, or otherwise change the value of appHistory’s ongoing navigation.

  19. For each disposedAHE of disposedAHEs:

    1. Fire an event named dispose at disposedAHE.

To get the app history index of a session history entry she within an AppHistory appHistory:
  1. Let index be 0.

  2. For each ahe of appHistory’s entry list:

    1. If ahe’s session history entry is equal to she, then return index.

    2. Increment index by 1.

  3. Assert: this step is never reached.

1.1. Introspecting the app history entry list

entries = appHistory.entries()

Returns an array of AppHistoryEntry instances representing the current app history list, i.e. all session history entries for this Window that are same origin and contiguous to the current session history entry.

appHistory.canGoBack

Returns true if the current AppHistoryEntry is not the first one in the app history entries list.

appHistory.canGoForward

Returns true if the current AppHistoryEntry is not the last one in the app history entries list.

The entries() method steps are:
  1. If this has entries and events disabled, then return the empty list.

  2. Return this's entries list.

The canGoBack getter steps are:
  1. If this has entries and events disabled, then return false.

  2. Assert: this's current index is not −1.

  3. If this's current index is 0, then return false.

  4. Return true.

The canGoForward getter steps are:
  1. If this has entries and events disabled, then return false.

  2. Assert: this's current index is not −1.

  3. If this's current index is equal to this's entry list's size − 1, then return false.

  4. Return true.

1.2. The current entry

appHistory.current

The current AppHistoryEntry.

appHistory.updateCurrent({ state })

Update the app history state of the current AppHistoryEntry, without performing a navigation like appHistory.reload() would do.

This method is best used to capture updates to the page that have already happened, and need to be reflected into the app history state. For cases where the state update is meant to drive a page update, instead use appHistory.navigate() or appHistory.reload().

The current entry for an AppHistory appHistory is the result running of the following algorithm:
  1. If this has entries and events disabled, then return null.

  2. Assert: this's current index is not −1.

  3. Return appHistory’s entry list[appHistory’s current index].

The current getter steps are to return the current entry for this.

The updateCurrent(options) method steps are:
  1. Let current be the current entry for this.

  2. If current is null, then throw an "InvalidStateError" DOMException.

  3. Let serializedState be StructuredSerializeForStorage(options["state"]), rethrowing any exceptions.

  4. Set current’s session history entry's app history state to serializedState.

1.3. Ongoing navigation tracking

[Exposed=Window]
interface AppHistoryTransition {
  readonly attribute AppHistoryNavigationType navigationType;
  readonly attribute AppHistoryEntry from;
  readonly attribute Promise<undefined> finished;

  AppHistoryResult rollback(optional AppHistoryNavigationOptions options = {});
};
appHistory.transition

An AppHistoryTransition object representing any ongoing navigation that hasn’t yet reached the navigatesuccess or navigateerror stage, if one exists, or null if there is no such transition ongoing.

Since appHistory.current (and other properties like location.href) are updated immediately upon navigation, this appHistory.transition property is useful for determining when such navigations are not yet fully settled, according to any promises passed to event.transitionWhile().

appHistory.transition.navigationType

One of "reload", "push", "replace", or "traverse", indicating what type of navigation this transition is for.

appHistory.transition.from

The AppHistoryEntry from which the transition is coming. This can be useful to compare against appHistory.current.

appHistory.transition.finished

A promise which fulfills at the same time the navigatesuccess event fires, or rejects at the same time the navigateerror fires.

{ committed, finished } = appHistory.transition.rollback()
{ committed, finished } = appHistory.transition.rollback({ info })

Aborts the ongoing navigation, and immediately performs another navigation which is the logical opposite of the one represented by this transition:

Aborting the ongoing navigation will cause navigateerror to fire, any navigateEvent.signal instances to fire abort, and any relevant promises to reject. This includes appHistory.transition.finished.

Then, the rollback navigation described above starts. This will fire a navigate event, and reset appHistory.transition to a new AppHistoryTransition instance. The info option, if provided, will populate the info property of the fired event.

This method can only be called while the transition is still ongoing, i.e. while appHistory.transition equals this AppHistoryTransition object. Calling it afterward will cause both returned promises rejected with an "InvalidStateError" DOMException.

An AppHistory has a transition, which is an AppHistoryTransition or null.

The transition getter steps are to return this's transition.


An AppHistoryTransition has an associated navigation type, which is an AppHistoryNavigationType.

An AppHistoryTransition has an associated from entry, which is an AppHistoryEntry.

An AppHistoryTransition has an associated finished promise, which is an Promise.

The navigationType getter steps are to return this's navigation type.

The from getter steps are to return this's from entry.

The finished getter steps are to return this's finished promise.

The rollback(options) method steps are:
  1. TODO use options.


During any given navigation, the AppHistory object needs to keep track of the following:

For all navigations
State Duration Explanation
The AppHistoryNavigateEvent For the duration of event firing So that if the navigation is canceled while the event is firing, we can cancel the event.
The event’s signal Until all promises passed to transitionWhile() have settled So that if the navigation is canceled, we can signal abort.
The AppHistoryEntry being navigated to From when it is determined, until all promises passed to transitionWhile() have settled So that we know what to resolve any committed and finished promises with.
Any finished Promise that was returned Until all promises passed to transitionWhile() have settled So that we can resolve or reject it appropriately.
For non-"traverse" navigations
State Duration Explanation
Any state For the duration of event firing So that we can update the current entry’s state after the event successfully finishes firing without being canceled.
For "traverse" navigations
State Duration Explanation
Any info Until the task is queued to fire the navigate event So that we can use it to fire the navigate event after the the trip through the session history traversal queue.
Any committed Promise that was returned Until the session history is updated (inside that same task) So that we can resolve or reject it appropriately.

Furthermore, we need to account for the fact that there might be multiple traversals queued up, e.g. via

const key1 = appHistory.entries()[appHistory.current.index - 1].key;
const key2 = appHistory.entries()[appHistory.current.index + 1].key;

appHistory.goTo(key1); // intentionally no await
appHistory.goTo(key2);

And, while non-traversal navigations cannot be queued in the same way since a new non-traversal navigation cancels an old one, we need to keep some state around so that we can properly cancel the old one. That is, given

const p1 = appHistory.navigate(url1).finished;
const p2 = appHistory.navigate(url2).finished;

we need to ensure that when navigating to url2, we still have the Promise p1 around so that we can reject it. We can’t just get rid of any ongoing navigation promises the moment the second call to navigate() happens.

We also need to ensure that, if we start a new navigation, navigations which have gotten as far as firing navigate events, but not yet as far as firing navigatesuccess or navigateerror, get finalized with an aborted navigation error.

We end up accomplishing all this using the following setup:

Each AppHistory object has an associated ongoing navigate event, an AppHistoryNavigateEvent or null, initially null.

Each AppHistory object has an associated ongoing navigation signal, which is an AbortSignal or null, initially null.

Each AppHistory object has an associated ongoing navigation, which is an app history API navigation or null, initially null.

Each AppHistory object has an associated upcoming non-traverse navigation, which is an app history API navigation or null, initially null.

Each AppHistory object has an associated upcoming traverse navigations, which is a map from strings to app history API navigations, initially empty.

An app history API navigation is a struct with the following items:

We need to store the ongoing navigation signal separately from the app history API navigation struct, since it needs to be tracked even for navigations that are not via the app history APIs.

To set the upcoming non-traverse navigation given an AppHistory appHistory, a JavaScript value info, and a serialized state-or-null serializedState:
  1. Let committedPromise and finishedPromise be new promises created in appHistory’s relevant Realm.

  2. Let ongoingNavigation be an app history API navigation whose app history is appHistory, key is null, info is info, serialized state is serializedState, committed-to entry is null, committed promise is committedPromise, finished promise is finishedPromise, and did finish before commit is false.

  3. Assert: appHistory’s upcoming non-traverse navigation is null.

  4. Set appHistory’s upcoming non-traverse navigation to ongoingNavigation.

  5. Return ongoingNavigation.

To set an upcoming traverse navigation given an AppHistory appHistory, a string key, and a JavaScript value info:
  1. Let committedPromise and finishedPromise be new promises created in appHistory’s relevant Realm.

  2. Let traversal be an app history API navigation whose whose app history is appHistory, key is key, info is info, serialized state is null, committed-to entry is null, committed promise is committedPromise, finished promise is finishedPromise, and did finish before commit is false.

  3. Set appHistory’s upcoming traverse navigations[key] to traversal.

  4. Return traversal.

To promote the upcoming navigation to ongoing given an AppHistory appHistory and a string-or-null destinationKey:
  1. Assert: appHistory’s ongoing navigation is null.

  2. If destinationKey is not null, then:

    1. Assert: appHistory’s upcoming non-traverse navigation is null.

    2. If appHistory’s upcoming traverse navigations[destinationKey] exists, then:

      1. Set appHistory’s ongoing navigation to appHistory’s upcoming traverse navigations[destinationKey].

      2. Remove appHistory’s upcoming traverse navigations[destinationKey].

  3. Otherwise,

    1. Set appHistory’s ongoing navigation to appHistory’s upcoming non-traverse navigation.

    2. Set appHistory’s upcoming non-traverse navigation to null.

To clean up an app history API navigation navigation:
  1. Let appHistory be navigation’s app history.

  2. If appHistory’s ongoing navigation is navigation, then set appHistory’s ongoing navigation to null.

  3. Otherwise,

    1. Assert: navigation’s key is not null.

    2. Assert: appHistory’s upcoming traverse navigations[navigation’s key] exists.

    3. Remove appHistory’s upcoming traverse navigations[navigation’s key].

To notify about the committed-to entry given an app history API navigation navigation and an AppHistoryEntry entry:
  1. Set navigation’s committed-to entry to entry.

  2. Resolve navigation’s committed promise with entry.

    After this point, navigation’s committed promise is only needed in cases where it has not yet been returned to author code. Implementations might want to clear it out to avoid keeping it alive for the lifetime of the app history API navigation.

  3. If navigation’s did finish before commit is true, then resolve the finished promise for navigation.

To resolve the finished promise for an app history API navigation navigation:
  1. If navigation’s finished promise is null, then return.

  2. If navigation’s committed-to entry entry is null, then:

    1. Set navigation’s did finish before commit to true.

    2. Return.

    In same-document traversal cases, resolve the finished promise can be called before notify about the committed-to entry, since the latter requires a roundtrip through the relevant session history traversal queue and the former just depends on the settlement of promises passed to transitionWhile().

  3. Resolve navigation’s finished promise with its committed-to entry.

  4. Clean up navigation.

To reject the finished promise for an app history API navigation navigation with a JavaScript value exception:
  1. If navigation’s finished promise is null, then return.

  2. Reject navigation’s finished promise with exception.

  3. If navigation’s committed promise is not null, then reject navigation’s committed promise with exception.

  4. Clean up navigation.

{ committed, finished } = appHistory.navigate(url)
{ committed, finished } = appHistory.navigate(url, options)

Navigates the current page to the given url. options can contain the following values:

  • replace can be set to true to replace the current session history entry, instead of pushing a new one.

  • info can be set to any value; it will populate the info property of the corresponding navigate event.

  • state can be set to any serializable value; it will populate the state retrieved by appHistory.current.getState() once the navigation completes, for same-document navigations. (It will be ignored for navigations that end up cross-document.)

By default this will perform a full navigation (i.e., a cross-document navigation, unless the given URL differs only in a fragment from the current one). The navigate event’s transitionWhile() method can be used to convert it into a same-document navigation.

The returned promises will behave as follows:

  • For navigations that get aborted, both promises will reject with an "AbortError" DOMException.

  • For same-document navigations created by using the navigate event’s transitionWhile() method, committed will fulfill immediately, and finished will fulfill or reject according to the promises passed to transitionWhile().

  • For other same-document navigations (e.g., non-intercepted fragment navigations), both promises will fulfill immediately.

  • For cross-document navigations, both promises will never settle.

In all cases, when the returned promises fulfill, it will be with the AppHistoryEntry that was navigated to.

{ committed, finished } = appHistory.reload(options)

Reloads the current page. The info and state options behave as described above.

The default behavior of performing a from-network-or-cache reload of the current page can be overriden by using the navigate event’s transitionWhile() method. Doing so will mean this call only updates state or passes along the appropriate info, plus performing whatever actions the navigate event handler sees fit to carry out.

The returned promises will behave as follows:

The navigate(url, options) method steps are:
  1. Parse url relative to this's relevant settings object. If that returns failure, then return an early error result for a "SyntaxError" DOMException. Otherwise, let urlRecord be the resulting URL record.

  2. If this's relevant global object's associated Document is not fully active, then return an early error result for an "InvalidStateError" DOMException.

  3. If this's relevant global object's associated Document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException.

  4. Let serializedState be null.

  5. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]). If this throws an exception, then return an early error result for that exception.

  6. Let info be options["info"] if it exists; otherwise, undefined.

  7. Let historyHandling be "replace" if options["replace"] is true; otherwise, "default".

  8. Return the result of performing a non-traverse app history navigation given this, urlRecord, serializedState, info, and historyHandling.

The reload(options) method steps are:
  1. If this's relevant global object's associated Document is not fully active, then return an early error result for an "InvalidStateError" DOMException.

  2. If this's relevant global object's associated Document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException.

  3. Let urlRecord be this's relevant global object's active document's URL.

  4. Let serializedState be null.

  5. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]). If this throws an exception, then return an early error result for that exception.

  6. Otherwise,

    1. Let current be the current entry of this.

    2. If current is not null, then set serializedState to current’s app history state.

  7. Let info be options["info"] if it exists; otherwise, undefined.

  8. Return the result of performing a non-traverse app history navigation given this, urlRecord, serializedState, info, and "reload".

To perform a non-traverse app history navigation given an AppHistory object appHistory, a URL url, a serialized state-or-null serializedState, a JavaScript value info, and a history handling behavior historyHandling:
  1. Let browsingContext be appHistory’s relevant global object's browsing context.

  2. Assert: browsingContext is not null.

  3. Assert: historyHandling is either "replace", "reload", or "default".

  4. Let ongoingNavigation be the result of setting the upcoming non-traverse navigation for appHistory given info and serializedState.

  5. Navigate browsingContext to url with historyHandling set to historyHandling, appHistoryState set to serializedState, and the source browsing context set to browsingContext.

  6. If appHistory’s upcoming non-traverse navigation is ongoingNavigation, then:

    This means the navigate algorithm bailed out before ever getting to the inner navigate event firing algorithm which would promote the upcoming navigation to ongoing.

    1. Set appHistory’s upcoming non-traverse navigation to null.

    2. Return an early error result for an "AbortError" DOMException.

  7. If ongoingNavigation’s serialized state is non-null, then set browsingContext’s session history's current entry's app history state to ongoingNavigation’s serialized state.

    At this point ongoingNavigation’s serialized state is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the app history API navigation.

  8. Return «[ "committed" → ongoingNavigation’s committed promise, "finished" → ongoingNavigation’s finished promise ]».

Unlike location.assign() and friends, which are exposed across origin-domain boundaries, appHistory.navigate() and appHistory.reload() can only be accessed by code with direct synchronous access to the appHistory property. Thus, we avoid the complications around tracking source browsing contexts, and we don’t need to deal with the allowed to navigate check and its accompanying exceptionsEnabled flag. We just treat all navigations as being initiated by the AppHistory object itself.

An an early error result for an exception e is a dictionary instance given by «[ "committed" → a promise rejected with e, "finished" → a promise rejected with e ]».

1.5. Traversing

{ committed, finished } = appHistory.goTo(key)
{ committed, finished } = appHistory.goTo(key, { info })

Traverses the joint session history to the closest joint session history entry that matches the AppHistoryEntry with the given key. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises will behave as follows:

{ committed, finished } = appHistory.back()
{ committed, finished } = appHistory.back({ info })

Traverse the joint session history to the closest previous joint session history entry which results in this frame navigating, i.e. results in appHistory.current updating. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises behave equivalently to those returned by goTo().

{ committed, finished } = appHistory.forward()
{ committed, finished } = appHistory.forward({ info })

Traverse the joint session history to the closest forward joint session history entry which results in this frame navigating, i.e. results in appHistory.current updating. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises behave equivalently to those returned by goTo().

The goTo(key, options) method steps are:
  1. If this's current index is −1, then return an early error result for an "InvalidStateError" DOMException.

  2. If this's entry list does not contain any AppHistoryEntry whose session history entry's app history key equals key, then return an early error result for an "InvalidStateError" DOMException.

  3. Return the result of performing an app history traversal given this, key, and options.

The back(options) method steps are:
  1. If this's current index is −1 or 0, then return an early error result for an "InvalidStateError" DOMException.

  2. Let key be this's entry list[this's current index − 1]'s session history entry's app history key.

  3. Return the result of performing an app history traversal given this, key, and options.

The forward(options) method steps are:
  1. If this's current index is −1 or is equal to this's entry list's size − 1, then return an early error result for an "InvalidStateError" DOMException.

  2. Let key be this's entry list[this's current index + 1]'s session history entry's app history key.

  3. Return the result of performing an app history traversal given this, key, and options.

The following algorithm is specified in terms of the session history rewrite pull request against the HTML Standard, because the existing session history traversal infrastructure is broken enough that it’s hard to build on. It is expected to track that work as it continues.

To perform an app history traversal given an AppHistory object appHistory, a string key, and an AppHistoryNavigationOptions options:

  1. If appHistory’s relevant global object's associated Document is not fully active, then return an early error result for an "InvalidStateError" DOMException.

  2. If appHistory’s relevant global object's associated Document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException.

  3. If appHistory’s current entry's session history entry's app history key equals key, then return «[ "committed" → a promise resolved with appHistory’s current entry, "finished" → a promise resolved with appHistory’s current entry

  4. If appHistory’s upcoming traverse navigations[key] exists, then:

    1. Let navigation be appHistory’s upcoming traverse navigations[key].

    2. Return «[ "committed" → navigation’s committed promise, "finished" → navigation’s finished promise ]».

  5. Let navigable be appHistory’s relevant global object's browsing context's containing navigable.

  6. Let traversable be navigable’s traversable navigable.

  7. Let initiatorBC be appHistory’s relevant global object's browsing context.

  8. Let info be options["info"] if it exists, or undefined otherwise.

  9. Let ongoingNavigation be the result of setting an upcoming traverse navigation for appHistory given key and info.

  10. Enqueue the following steps on traversable’s session history traversal queue:

    1. Let navigableEntries be the result of getting the session history entries given navigable.

    2. Let targetEntry be the session history entry in navigableEntries whose app history key equals key. If no such entry exists, then:

      1. Reject the finished promise for ongoingNavigation with an "InvalidStateError" DOMException.

      2. Abort these steps.

      This can occur if the appHistory object’s view of session history is outdated, which can happen for brief periods while all the relevant threads and processes are being synchronized in reaction to a history change (such as the user clearing their history).

    3. If targetEntry is navigable’s active session history entry, then abort these steps.

      This can occur if a previously-queued-up traversal already took us to this session history entry. In that case that previous traversal will have dealt with ongoingNavigation already.

    4. Let targetStep be null.

    5. If targetEntry’s step is greater than traversable’s current session history step, then set targetStep to targetEntry’s step.

    6. Otherwise:

      1. Let afterTarget be the session history entry after targetEntry in navigableEntries.

      2. Let allSteps be the result of getting all history steps that are part of the target session TODO.

      3. Set targetStep to the greatest number in allSteps that is less than afterTarget’s step.

    7. Apply the history step targetStep to traversable, with true, initiatorBC, and "none".

      Eventually apply the history step will have well-specified hooks for communicating these conditions back to its caller.

  11. Return «[ "committed" → ongoingNavigation’s committed promise, "finished" → ongoingNavigation’s finished promise ]».

1.6. Event handlers

The following are the event handlers (and their corresponding event handler event types) that must be supported, as event handler IDL attributes, by objects implementing the AppHistory interface:

Event handler Event handler event type
onnavigate navigate
onnavigatesuccess navigatesuccess
onnavigateerror navigateerror
[Exposed=Window]
interface AppHistoryNavigateEvent : Event {
  constructor(DOMString type, AppHistoryNavigateEventInit eventInit);

  readonly attribute AppHistoryNavigationType navigationType;
  readonly attribute AppHistoryDestination destination;
  readonly attribute boolean canTransition;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  readonly attribute any info;

  undefined transitionWhile(Promise<undefined> newNavigationAction);
};

dictionary AppHistoryNavigateEventInit : EventInit {
  AppHistoryNavigationType navigationType = "push";
  required AppHistoryDestination destination;
  boolean canTransition = false;
  boolean userInitiated = false;
  boolean hashChange = false;
  required AbortSignal signal;
  FormData? formData = null;
  any info;
};

enum AppHistoryNavigationType {
  "reload",
  "push",
  "replace",
  "traverse"
};
event.navigationType

One of "reload", "push", "replace", or "traverse", indicating what type of navigation this is.

event.destination

An AppHistoryDestination representing the destination of the navigation.

event.canTransition

True if transitionWhile() can be called to convert this navigation into a single-page navigation; false otherwise.

Generally speaking, this will be true whenever the current Document can have its URL rewritten to the destination URL, except for cross-document back/forward navigations, where it will always be false.

event.userInitiated

True if this navigation was due to a user clicking on an a element, submitting a form element, or using the browser UI to navigate; false otherwise.

event.hashChange

True if this navigation is a fragment navigation; false otherwise.

event.signal

An AbortSignal which will become aborted if the navigation gets canceled, e.g. by the user pressing their browser’s "Stop" button, or another higher-priority navigation interrupting this one.

The expected pattern is for developers to pass this along to any async operations, such as fetch(), which they perform as part of handling this navigation.

event.formData

The FormData representing the submitted form entries for this navigation, if this navigation is a "push" or "replace" navigation representing a POST form submission; null otherwise.

(Notably, this will be null even for "reload" and "traverse" navigations that are revisiting a session history entry that was originally created from a form submission.)

event.info

An arbitrary JavaScript value passed via other app history APIs that initiated this navigation, or null if the navigation was initiated by the user or via a non-app history API.

event.transitionWhile(newNavigationAction)

Synchronously converts this navigation into a same-document navigation to the destination URL.

The given newNavigationAction promise is used to signal the duration, and success or failure, of the navigation. After it settles, the browser signals to the user (e.g. via a loading spinner UI, or assistive technology) that the navigation is finished. Additionally, it fires navigatesuccess or navigateerror events as appropriate, which other parts of the web application can respond to.

This method will throw a "SecurityError" DOMException if canTransition is false, or if isTrusted is false. It will throw an "InvalidStateError" DOMException if not called synchronously, during event dispatch.

The navigationType, destination, canTransition, userInitiated, hashChange, signal, formData, and info getter steps are to return the value that the corresponding attribute was initialized to.

An AppHistoryNavigateEvent has the following associated values which are only conditionally used:

This is set appropriately when the event is fired.

An AppHistoryNavigateEvent also has an associated navigation action promises list, which is a list of Promise objects, initially empty.

The transitionWhile(newNavigationAction) method steps are:
  1. If this's relevant global object's active Document is not fully active, then throw an "InvalidStateError" DOMException.

  2. If this's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException.

  3. If this's canTransition attribute was initialized to false, then throw a "SecurityError" DOMException.

  4. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException.

  5. If this's canceled flag is set, then throw an "InvalidStateError" DOMException.

  6. Append newNavigationAction to this's navigation action promises list.

[Exposed=Window]
interface AppHistoryDestination {
  readonly attribute USVString url;
  readonly attribute DOMString? key;
  readonly attribute DOMString? id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();
};
event.destination.url

The URL being navigated to.

event.destination.key

The value of the key property of the destination AppHistoryEntry, if this is a "traverse" navigation, or null otherwise.

event.destination.id

The value of the id property of the destination AppHistoryEntry, if this is a "traverse" navigation, or null otherwise.

event.destination.index

The value of the index property of the destination AppHistoryEntry, if this is a "traverse" navigation, or −1 otherwise.

event.destination.sameDocument

Indicates whether or not this navigation is to the same Document as the current document value, or not. This will be true, for example, in cases of fragment navigations or history.pushState() navigations.

Note that this property indicates the original nature of the navigation. If a cross-document navigation is converted into a same-document navigation using event.transitionWhile(), that will not change the value of this property.

state = event.destination.getState()

For "traverse" navigations, returns the deserialization of the state stored in the destination session history entry.

For "push" and "replace" navigations, returns the deserialization of the state passed to appHistory.navigate(), if the navigation was initiated in that way, or undefined if it wasn’t.

For "reload" navigations, returns the deserialization of the state passed to appHistory.reload(), if the reload was initiated in that way, or undefined if it wasn’t.

An AppHistoryDestination has an associated URL, which is a URL.

An AppHistoryDestination has an associated key, which is a string-or-null.

An AppHistoryDestination has an associated id, which is a string-or-null.

An AppHistoryDestination has an associated index, which is an integer.

An AppHistoryDestination has an associated state, which is a serialized state-or-null.

An AppHistoryDestination has an associated is same document, which is a boolean.

The url getter steps are to return this's URL, serialized.

The key getter steps are to return this's key.

The id getter steps are to return this's id.

The index getter steps are to return this's index.

The sameDocument getter steps are to return this's is same document.

The getState() method steps are:
  1. If this's state is null, then return undefined.

  2. Return StructuredDeserialize(this's state).

To fire a traversal navigate event at an AppHistory appHistory given a session history entry destinationEntry, and an optional user navigation involvement userInvolvement (default "none"):
  1. Let event be the result of creating an event given AppHistoryNavigateEvent, in appHistory’s relevant Realm.

  2. Let destination be a new AppHistoryDestination created in appHistory’s relevant Realm.

  3. Set destination’s URL to destinationEntry’s URL.

  4. If destinationEntry’s origin is same origin with appHistory’s relevant settings object's origin, then:

    1. Set destination’s key to destinationEntry’s app history key.

    2. Set destination’s id to destinationEntry’s app history id.

    3. Set destination’s index to the result of getting the app history index of destinationEntry within appHistory.

    4. Set destination’s state to destinationEntry’s app history state.

  5. Otherwise,

    1. Set destination’s key to null.

    2. Set destination’s id to null.

    3. Set destination’s index to −1.

    4. Set destination’s state to null.

  6. Set destination’s is same document to true if destinationEntry’s document is equal to appHistory’s relevant global object's associated Document; otherwise false.

  7. Return the result of performing the inner navigate event firing algorithm given appHistory, "traverse", event, destination, userInvolvement, and null.

To fire a push or replace navigate event at an AppHistory appHistory given an AppHistoryNavigationType navigationType, a URL destinationURL, a boolean isSameDocument, an optional user navigation involvement userInvolvement (default "none"), an optional serialized state-or-null state (default null), an optional list of FormData entries or null formDataEntryList (default null), and an optional serialized state-or-null classicHistoryAPISerializedData (default null):
  1. Let event be the result of creating an event given AppHistoryNavigateEvent, in appHistory’s relevant Realm.

  2. Set event’s classic history API serialized data to classicHistoryAPISerializedData.

  3. Let destination be a new AppHistoryDestination created in appHistory’s relevant Realm.

  4. Set destination’s URL to destinationURL.

  5. Set destination’s key to null.

  6. Set destination’s id to null.

  7. Set destination’s index to −1.

  8. Set destination’s state to state.

  9. Set destination’s is same document to isSameDocument.

  10. Return the result of performing the inner navigate event firing algorithm given appHistory, navigationType, event, destination, userInvolvement, and formDataEntryList.

The inner navigate event firing algorithm is the following steps, given an AppHistory appHistory, an AppHistoryNavigationType navigationType, an AppHistoryNavigateEvent event, an AppHistoryDestination destination, a user navigation involvement userInvolvement, and a list of FormData entries or null formDataEntryList:
  1. Promote the upcoming navigation to ongoing given appHistory and destination’s key.

  2. Let ongoingNavigation be appHistory’s ongoing navigation.

  3. Let document be appHistory’s relevant global object's associated document.

  4. If document can have its URL rewritten to destination’s URL, and either destination’s is same document is true or navigationType is not "traverse", then initialize event’s canTransition to true. Otherwise, initialize it to false.

  5. If either userInvolvement is not "browser UI" or navigationType is not "traverse", then initialize event’s cancelable to true. Otherwise, initialize it to false.

  6. If both event’s canTransition and event’s cancelable are false, then return true.

    In this case we are definitely performing a cross-document navigation or traversal. We don’t clean up ongoingNavigation however, since we might end up finalizing with an aborted navigation error before the current Document unloads.

  7. If appHistory has entries and events disabled, then:

    1. If ongoingNavigation is not null, then clean up ongoingNavigation.

      In this case the committed promise and finished promise will never fulfill, since we never create AppHistoryEntrys for the initial about:blank Document so we have nothing to resolve them with.

    2. Return true.

  8. Initialize event’s type to "navigate".

  9. Initialize event’s navigationType to navigationType.

  10. Initialize event’s destination to destination.

  11. If ongoingNavigation is not null, then initialize event’s info to ongoingNavigation’s info. Otherwise, initialize it to undefined.

    At this point ongoingNavigation’s info is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the app history API navigation.

  12. Initialize event’s signal to a new AbortSignal created in appHistory’s relevant Realm.

  13. Let currentURL be document’s URL.

  14. If all of the following are true:

    then initialize event’s hashChange to true. Otherwise, initialize it to false.

  15. If userInvolvement is "none", then initialize event’s userInitiated to false. Otherwise, initialize it to true.

  16. If formDataEntryList is not null, then initialize event’s formData to a new FormData created in appHistory’s relevant Realm, associated to formDataEntryList. Otherwise, initialize it to null.

  17. Assert: appHistory’s ongoing navigate event is null.

  18. Set appHistory’s ongoing navigate event to event.

  19. Assert: appHistory’s ongoing navigation signal is null.

  20. Set appHistory’s ongoing navigation signal to event’s signal.

  21. Let dispatchResult be the result of dispatching event at appHistory.

  22. Set appHistory’s ongoing navigate event to null.

  23. If dispatchResult is false:

    1. If navigationType is not "traverse" and event’s signal's aborted flag is unset, then finalize with an aborted navigation error given appHistory and ongoingNavigation.

      If navigationType is "traverse", then we will finalize with an aborted navigation error in perform an app history traversal.

    2. Return false.

  24. Let hadTransitionWhile be true if event’s navigation action promises list is not empty; otherwise false.

  25. Let endResultIsSameDocument be true if hadTransitionWhile is true or destination’s is same document is true.

  26. If hadTransitionWhile is true:

    1. Let fromEntry be the current entry for appHistory.

    2. Assert: fromEntry is not null.

    3. Set appHistory’s transition to a new AppHistoryTransition created in appHistory’s relevant Realm, whose navigation type is navigationType, from entry is fromEntry, and whose finished promise is a new promise created in appHistory’s relevant Realm.

  27. If endResultIsSameDocument is true:

    1. Let transition be appHistory’s transition.

    2. Assert: transition is not null.

    3. Wait for all of event’s navigation action promises list, with the following success steps:

      1. If event’s signal's aborted flag is set, then abort these steps.

      2. Fire an event named navigatesuccess at appHistory.

      3. Resolve transition’s finished promise with undefined.

      4. If appHistory’s transition is transition, then set appHistory’s transition to null.

      5. If ongoingNavigation is non-null, then resolve the finished promise for ongoingNavigation.

      and the following failure steps given reason rejectionReason:
      1. If event’s signal's aborted flag is set, then abort these steps.

      2. Fire an event named navigateerror at appHistory using ErrorEvent, with error initialized to rejectionReason, and message, filename, lineno, and colno initialized to appropriate values that can be extracted from rejectionReason in the same underspecified way the user agent typically does for the report an exception algorithm.

      3. Reject transition’s finished promise with rejectionReason.

      4. If appHistory’s transition is transition, then set appHistory’s transition to null.

      5. If ongoingNavigation is non-null, then reject the finished promise for ongoingNavigation with rejectionReason.

  28. Otherwise, if ongoingNavigation is non-null, then:

    1. Set ongoingNavigation’s serialized state to null.

      This ensures that any call to appHistory.navigate() which triggered this algorithm does not overwrite the app history state of the current entry for cross-document navigations.

    2. Clean up ongoingNavigation.

  29. If hadTransitionWhile is true and navigationType is not "traverse":

    1. Let isPush be true if navigationType is "push"; otherwise, false.

    2. Run the URL and history update steps given document and event’s destination's URL, with serializedData set to event’s classic history API serialized data and isPush set to isPush.

    3. Return false.

  30. Return true.

To finalize with an aborted navigation error given an AppHistory appHistory, an app history API navigation or null ongoingNavigation, and an optional DOMException error:
  1. If appHistory’s ongoing navigate event is non-null, then:

    1. Set appHistory’s ongoing navigate event's canceled flag to true.

    2. Set appHistory’s ongoing navigate event to null.

  2. If appHistory’s ongoing navigation signal is non-null, then:

    1. Signal abort on appHistory’s ongoing navigation signal.

    2. Set appHistory’s ongoing navigation signal to null.

  3. If error was not given, then set error to a new "AbortError" DOMException, created in appHistory’s relevant Realm.

  4. Fire an event named navigateerror at appHistory using ErrorEvent, with error initialized to error, and message, filename, lineno, and colno initialized to appropriate values that can be extracted from error and the current JavaScript stack in the same underspecified way the user agent typically does for the report an exception algorithm.

    Thus, for example, if this algorithm is reached because of a call to window.stop(), these properties would probably end up initialized based on the line of script that called window.stop(). But if it’s because the user clicked the stop button, these properties would probably end up with default values like the empty string or 0.

  5. If ongoingNavigation is non-null, then:

    1. Set ongoingNavigation’s serialized state to null.

      This ensures that any call to appHistory.navigate() which triggered this algorithm does not overwrite the app history state of the current entry for aborted navigations.

    2. Reject the finished promise for ongoingNavigation with error.

  6. If appHistory’s transition is not null, then:

    1. Reject appHistory’s transition's finished promise with error.

    2. Set appHistory’s transition to null.

To inform app history about canceling navigation in a browsing context bc:
  1. Let appHistory be bc’s active window's app history.

  2. If appHistory’s ongoing navigation signal is null, then return.

  3. Finalize with an aborted navigation error given appHistory and appHistory’s ongoing navigation.

To inform app history about browsing context discarding given a browsing context bc:
  1. Inform app history about canceling navigation in bc.

  2. Let appHistory be bc’s active window's app history.

  3. Let traversals be a clone of appHistory’s upcoming traverse navigations.

  4. For each traversal of traversals: finalize with an aborted navigation error given appHistory and traversal.

3. App history entries

[Exposed=Window]
interface AppHistoryEntry : EventTarget {
  readonly attribute USVString url;
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();

  attribute EventHandler onnavigateto;
  attribute EventHandler onnavigatefrom;
  attribute EventHandler onfinish;
  attribute EventHandler ondispose;
};
entry.url

The URL of this app history entry.

entry.key

A user agent-generated random UUID string representing this app history entry’s place in the app history list. This value will be reused by other AppHistoryEntry instances that replace this one due to replace-style navigations. This value will survive session restores.

This is useful for navigating back to this location in the app history entry list, using appHistory.goTo(key).

entry.id

A user agent-generated random UUID string representing this specific app history entry. This value will not be reused by other AppHistoryEntry instances. This value will survive session restores.

This is useful for associating data with this app history entry using other storage APIs.

entry.index

The index of this app history entry within appHistory.entries(), or −1 if the entry is not in the app history list.

entry.sameDocument

Indicates whether or not this app history entry is for the same Document as the current document value, or not. This will be true, for example, when the entry represents a fragment navigation or single-page app navigations.

state = entry.getState()

Returns the deserialization of the state stored in this entry, which was added to the entry using appHistory.navigate(). This state survives session restores.

Note that in general, unless the state value is a primitive, entry.getState() !== entry.getState(), since a fresh copy is returned each time.

This state is unrelated to the classic history API’s history.state.

Each AppHistoryEntry has an associated session history entry, which is a session history entry.

Each AppHistoryEntry has an associated index, which is an integer.

The key getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return the empty string.

  2. Return this's session history entry's app history key.

The id getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return the empty string.

  2. Return this's session history entry's app history id.

The url getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return the empty string.

  2. Return this's session history entry's URL, serialized.

The index getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return −1.

  2. Return this's index.

The sameDocument getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return false.

  2. Return true if this's session history entry's document equals this's relevant global object's associated Document, and false otherwise.

The getState() method steps are:
  1. If this's relevant global object's associated Document is not fully active, then return undefined.

  2. If this's session history entry's app history state is null, then return undefined.

  3. Return StructuredDeserialize(this's session history entry's app history state).

Unlike history.state, this will deserialize upon each access.

This can in theory throw an exception, if attempting to deserialize a large ArrayBuffer when not enough memory is available.

The following are the event handlers (and their corresponding event handler event types) that must be supported, as event handler IDL attributes, by objects implementing the AppHistoryEntry interface:

Event handler Event handler event type
onnavigateto navigateto
onnavigatefrom navigatefrom
onfinish finish
ondispose dispose

TODO: actually fire finish, navigateto, and navigatefrom.

The following section details monkeypatches to [HTML] that cause the navigate event to be fired appropriately, and for canceling the event to cancel the navigation. The first few sections detail slight tweaks to existing algorithms to pass through useful information into the navigation and history traversal algorithms. Then, § 4.3 Navigation algorithm updates contains the actual firing of the event.

4.1. Form submission patches

To properly thread the form entry list from its creation through to AppHistoryNavigateEvent's formData property, we need the following modifications:

Modify the navigate algorithm to take a list of entries or null entryList (default null), replacing its navigationType parameter. Then insert a step somewhere early in the algorithm to convert this back into the navigationType variable used by the in parallel section that is ultimately passed to [CSP]:
  1. Let navigationType be "form-submission" if entryList is non-null; otherwise, "other".

Modify the plan to navigate algorithm to take an additional optional argument entryList (default null). Then, modify the step which calls navigate to pass it along:
  1. Navigate target browsing context to destination, with historyHandling set to historyHandling and navigationType set to "form-submission" entryList set to entryList .

Modify the submit as entity body algorithm to pass entry list along to plan to navigate as a second argument.

4.2. Browser UI/user-initiated patches

To more rigorously specify when a navigation is initiated from browser UI or by the user interacting with a, area, and form elements, both for the purposes of the AppHistoryNavigateEvent's userInitiated property and for prohibiting interception of certain types of browser-UI-initiated navigations, we need the following modifications:

Introduce (right before the definition of the navigate algorithm) the concept of a user navigation involvement, which is one of the following:

"browser UI"

The navigation was initiated by the user via browser UI mechanisms

"activation"

The navigation was initiated by the user via the activation behavior of an element

"none"

The navigation was not initiated by the user

Define the user navigation involvement for an Event event as "activation" if event’s isTrusted attribute is initialized to true, and "none" otherwise.

Modify the navigate algorithm to take an optional named argument userInvolvement (default "none"). Then, update the paragraph talking about browser-UI initiated navigation as follows:

A user agent may provide various ways for the user to explicitly cause a browsing context to navigate, in addition to those defined in this specification. Such cases must set the userInvolvement argument to "browser UI".

This infrastructure partially solves whatwg/html#5381, and it’d be ideal to update the `Sec-Fetch-Site` spec at the same time.

Modify the navigate to a fragment algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the traverse the history by a delta argument to take an optional named argument userInvolvement (default "none"). Then, update the paragraph talking about user-initiated navigation as follows:

When the user navigates through a browsing context, e.g. using a browser’s back and forward buttons, the user agent must traverse the history by a delta with a delta equivalent to the action specified by the user and , the browsing context being operated on , and userInvolvement set to "browser UI" .

Modify the follow the hyperlink algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the activation behavior of area elements by introducing the event argument and replacing the follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event.

Modify the activation behavior of a elements by replacing its follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event.

Expand the section on "Providing users with a means to follow hyperlinks created using the link element" by adding the following sentence:

Such invocations of follow the hyperlink algorithm must set the userInvolvement argument to "browser UI".

Modify the plan to navigate algorithm to take a userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the submit algorithm to take an optional userInvolvement argument (default "none"). Have the submit algorithm pass along its value to all invocations of plan to navigate.

Modify the definition of the activation behavior for input elements to take an event argument. Then, pass along this argument to the invocation of the input activation behavior.

Modify the Submit Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the Image Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the button element’s activation behavior by having it take an event argument and, in the Submit Button case, to pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the no-submit button case for implicit form submission to pass along "activation" as the final argument when it calls submit.

The case of implicit submission when a submit button is present is automatically taken care of because it fires a (trusted) click event at the submit button.

With the above infrastructure in place, we can actually fire and handle the navigate event in the following locations:

Modify the shared history push/replace state steps by inserting the following steps right before the step that runs the URL and history update steps.
  1. Let appHistory be history’s relevant global object's app history.

  2. Let navigationType be "push" if isPush is true, and "replace" otherwise.

  3. Let continue be the result of firing a push or replace navigate event at appHistory with navigationType set to navigationType, isSameDocument set to true, destinationURL set to newURL, and classicHistoryAPISerializedData set to serializedData.

  4. If continue is false, return.

Modify the navigate to a fragment algorithm by prepending the following steps. Recall that per § 4.2 Browser UI/user-initiated patches we have introduced a userInvolvement argument.
  1. Let appHistory be the current entry's document’s relevant global object's app history.

  2. Let navigationType be the result of converting a history handling behavior to a navigation type given historyHandling.

  3. Let continue be the result of firing a push or replace navigate event at appHistory given with navigationType set to navigationType, isSameDocument set to true, userInvolvement set to userInvolvement, and destinationURL set to url.

  4. If continue is false, return.

To convert a history handling behavior to a navigation type given a history handling behavior historyHandling:
  1. Assert: historyHandling is not "entry update".

  2. Return the result of switching on historyHandling:

    "reload"

    "reload"

    "replace"

    "replace"

    "default"

    "push"

The traverse the history by a delta algorithm will be totally re-written as part of the session history rewrite. Here we reproduce the final version of the algorithm, after both that rewrite and with appropriate app history updates. Recall that per § 4.2 Browser UI/user-initiated patches we have introduced a userInvolvement argument, so the arguments are now delta, source browsing context, and userInvolvement.
  1. Let traversable be source browsing context’s containing navigable's traversable navigable.

  2. Let initiatorOrigin be source browsing context’s active document's origin.

  3. Enqueue the following steps to traversable’s session history traversal queue:

    1. Let allSteps be the result of getting all history steps for traversable.

    2. Let currentStepIndex be the index of the current session history step within allSteps.

    3. Let targetStepIndex be currentStepIndex plus delta.

    4. If allSteps[targetStepIndex] does not exist, then return.

    5. Apply the history step allSteps[targetStepIndex] to traversable with true, step, initiatorOrigin, and userInvolvement.

Modify the apply the history step algorithm as follows. Change checkForUserCancellation to fireBeforeunloadAndNavigate. Add the userInvolvement parameter. Then, insert the following step after step 12 (which assembles toTraverse and other lists) but before step 13 (which checks if unloading is user-cancelled):
  1. If fireBeforeunloadAndNavigate is true and the result of firing traversal navigate events given toTraverse, step, initiatorToCheck, and userInvolvement is false, then return.

To fire traversal navigate events for a list of navigables toTraverse, an integer step, an origin initiatorOrigin, and a user navigation involvement userInvolvement:
  1. Let overallResult be true.

  2. Let totalTasks be the size of toTraverse.

  3. Let completedTasks be 0.

  4. For each navigable of toTraverse, queue a global task on the history traversal task source given navigable’s active document's relevant global object to run these steps:

    1. Let destinationEntry be the item in the result of getting the session history entries for navigable that has the greatest step less than or equal to step.

    2. If destinationEntry’s document is not equal to navigable’s active document, and initiatorOrigin is not same origin-domain with navigable’s active document's origin, then abort these steps.

    3. Let appHistory be navigable’s active document's relevant global object's app history.

    4. Let result be the result of firing a traversal navigate event at appHistory with destinationEntry set to destinationEntry and userInvolvement set to userInvolvement.

    5. If result is false, then set overallResult to false.

    6. Increment completedTasks.

  5. Wait for completedTasks to be totalTasks.

  6. Return overallResult.

5. Patches to session history

This section details monkeypatches to [HTML] to track appropriate data for associating an AppHistory with a session history entry.

5.1. New session history entry items

Each session history entry gains the following new items:

5.2. Carrying over the app history key

Update the update the session history with the new page algorithm’s "replace" case by adding the following step after the construction of newEntry:
  1. If newEntry’s origin is the same as sessionHistory’s current entry's origin, then set newEntry’s app history key to sessionHistory’s current entry's app history key.

5.3. Carrying over the app history state

Update the navigate to a fragment algorithm by updating the step which creates and appends a new session history entry to carry over the app history state from the current entry as well.

5.4. Tracking the origin member

Update the update the session history with the new page algorithm’s "replace" and "default" cases to set newEntry’s origin to newDocument’s origin as part of its creation.

Update the navigate to a fragment algorithm to set the new session history entry's origin to the current entry's document's origin.

Update the URL and history update steps algorithm to set the new session history entry's origin to document’s origin.

Potentially update the traverse the history algorithm to consult the new origin field, instead of checking the document's origin, since the document can disappear?? Needs further investigation.

5.5. Updating the AppHistory object

Update the traverse the history algorithm by adding the following step before the final step which fires various events:
  1. Update the entries of newDocument’s relevant global object's app history.

Update the URL and history update steps by appending the following final step:
  1. Update the entries of document’s relevant global object's app history.

We do not update the entries when initially creating a new browsing context, as we intentionally don’t want to include the initial about:blank Document in any app history entry list.

6. Other patches

6.1. Canceling navigation and traversals

The existing HTML specification discusses canceling a navigation and traverals in a few places. However, the process is not very well-defined, and per whatwg/html#6927, is not very interoperable. We plan to make it more rigorous, after the session history rewrite lands.

Specifically, the spec uses a few phrases:

whatwg/html#6927 reveals that implementations don’t really follow this breakdown. In particular, modulo one case in Firefox, traversals are only canceled as part of discarding a browsing context.

That leaves us with two main operations: canceling not-yet-mature navigations, and dealing with browsing context discarding.


App history introduces a new complication here, which is that a navigation might have matured but still be "ongoing", in the sense of § 1.3 Ongoing navigation tracking. That is, consider a case such as:

appHistory.addEventListener("navigate", e => {
  e.transitionWhile(new Promise(r => setTimeout(r, 1_000)));
  e.signal.addEventListener("abort", () => { ... });
});

const p = appHistory.navigate("#1");

setTimeout(() => window.stop(), 500);

Without the navigate event handler, this kind of synchronous fragment navigation would be straightforward: it matures synchronously, and the stop() call does nothing. But because we have used the navigate handler to indicate that the navigation is still ongoing, we want the stop() call to finalize that navigation with an aborted navigation error, in particular causing p to reject and the abort event to fire on e.signal.


The integration is then as follows:

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WebIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/

Informative References

[CSP]
Mike West. Content Security Policy Level 3. URL: https://w3c.github.io/webappsec-csp/
[WEBAPPSEC-FETCH-METADATA-1]
Fetch Metadata Request Headers URL: https://w3c.github.io/webappsec-fetch-metadata/

IDL Index

partial interface Window {
  readonly attribute AppHistory appHistory;
};

[Exposed=Window]
interface AppHistory : EventTarget {
  sequence<AppHistoryEntry> entries();
  readonly attribute AppHistoryEntry? current;
  undefined updateCurrent(AppHistoryUpdateCurrentOptions options);
  readonly attribute AppHistoryTransition? transition;

  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;

  AppHistoryResult navigate(USVString url, optional AppHistoryNavigateOptions options = {});
  AppHistoryResult reload(optional AppHistoryReloadOptions options = {});

  AppHistoryResult goTo(DOMString key, optional AppHistoryNavigationOptions options = {});
  AppHistoryResult back(optional AppHistoryNavigationOptions options = {});
  AppHistoryResult forward(optional AppHistoryNavigationOptions options = {});

  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
};

dictionary AppHistoryUpdateCurrentOptions {
  required any state;
};

dictionary AppHistoryNavigationOptions {
  any info;
};

dictionary AppHistoryNavigateOptions : AppHistoryNavigationOptions {
  any state;
  boolean replace = false;
};

dictionary AppHistoryReloadOptions : AppHistoryNavigationOptions {
  any state;
};

dictionary AppHistoryResult {
  Promise<AppHistoryEntry> committed;
  Promise<AppHistoryEntry> finished;
};

[Exposed=Window]
interface AppHistoryTransition {
  readonly attribute AppHistoryNavigationType navigationType;
  readonly attribute AppHistoryEntry from;
  readonly attribute Promise<undefined> finished;

  AppHistoryResult rollback(optional AppHistoryNavigationOptions options = {});
};

[Exposed=Window]
interface AppHistoryNavigateEvent : Event {
  constructor(DOMString type, AppHistoryNavigateEventInit eventInit);

  readonly attribute AppHistoryNavigationType navigationType;
  readonly attribute AppHistoryDestination destination;
  readonly attribute boolean canTransition;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  readonly attribute any info;

  undefined transitionWhile(Promise<undefined> newNavigationAction);
};

dictionary AppHistoryNavigateEventInit : EventInit {
  AppHistoryNavigationType navigationType = "push";
  required AppHistoryDestination destination;
  boolean canTransition = false;
  boolean userInitiated = false;
  boolean hashChange = false;
  required AbortSignal signal;
  FormData? formData = null;
  any info;
};

enum AppHistoryNavigationType {
  "reload",
  "push",
  "replace",
  "traverse"
};

[Exposed=Window]
interface AppHistoryDestination {
  readonly attribute USVString url;
  readonly attribute DOMString? key;
  readonly attribute DOMString? id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();
};

[Exposed=Window]
interface AppHistoryEntry : EventTarget {
  readonly attribute USVString url;
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();

  attribute EventHandler onnavigateto;
  attribute EventHandler onnavigatefrom;
  attribute EventHandler onfinish;
  attribute EventHandler ondispose;
};