diff --git a/spec.bs b/spec.bs index 00aab6a..a8cce92 100644 --- a/spec.bs +++ b/spec.bs @@ -58,10 +58,7 @@ spec: html; urlPrefix: https://whatpr.org/html/6315/ text: get all history steps; for: traversable navigable; url: history.html#getting-all-history-steps text: step; for: session history entry; url: history.html#she-step text: apply the history step; url: history.html#apply-the-history-step - text: checkForUserCancelation; for: apply the history step; url: history.html#apply-the-history-step - text: initiatorToCheck; for: apply the history step; url: history.html#apply-the-history-step text: containing navigable; for: browsing context; url: browsers.html#bc-navigable - text: session; for: browsing context group; url: browsers.html#bcg-session text: active document; for: navigable; url: history.html#nav-document text: navigable; url: history.html#navigable spec: uuid; type: dfn; urlPrefix: https://wicg.github.io/uuid/ @@ -157,6 +154,10 @@ interface AppHistory : EventTarget { Promise navigate(USVString url, optional AppHistoryNavigateOptions options = {}); Promise reload(optional AppHistoryReloadOptions options = {}); + Promise goTo(DOMString key, optional AppHistoryNavigationOptions options = {}); + Promise back(optional AppHistoryNavigationOptions options = {}); + Promise forward(optional AppHistoryNavigationOptions options = {}); + attribute EventHandler onnavigate; attribute EventHandler onnavigatesuccess; attribute EventHandler onnavigateerror; @@ -176,16 +177,6 @@ dictionary AppHistoryReloadOptions : AppHistoryNavigationOptions { }; -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. - -Each {{AppHistory}} object has an associated ongoing navigate event, an {{AppHistoryNavigateEvent}} or null, initially null. - -Each {{AppHistory}} object has an associated navigate method call promise, which is either a {{Promise}} or null, initially null. - -Each {{AppHistory}} object has an associated navigate method call serialized state, which is either a [=serialized state=] or null, initially null. -

Introspecting the app history entry list

@@ -210,6 +201,10 @@ Each {{AppHistory}} object has an associated navigate meth
+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. +
The current getter steps are: @@ -346,6 +341,143 @@ Each {{AppHistory}} object has an associated navigate meth 1. Assert: this step is never reached.
+

Ongoing navigation tracking

+ +During any given navigation, the {{AppHistory}} object needs to keep track of the following: + + + + + + + + + +
For non-"{{AppHistoryNavigationType/traverse}}" 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 [=Event/canceled flag|cancel=] the event. +
Any {{AppHistoryNavigateOptions/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. +
The event's {{AppHistoryNavigateEvent/signal}} + Until all promises passed to {{AppHistoryNavigateEvent/respondWith()}} have settled + So that if the navigation is canceled, we can [=AbortSignal/signal abort=]. +
Any {{Promise}} that was returned + Until all promises passed to {{AppHistoryNavigateEvent/respondWith()}} have settled + So that we can [=resolve=] or [=reject=] it appropriately. +
+ + + + + + + + + +
For "{{AppHistoryNavigationType/traverse}}" 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 [=Event/canceled flag|cancel=] the event. +
Any {{AppHistoryNavigationOptions/navigateInfo}} + Until the task is queued to fire the {{AppHistory/navigate}} event + So that we can use it to fire the {{AppHistory/navigate}} event after the the trip through the [=traversable navigable/session history traversal queue=] +
The event's {{AppHistoryNavigateEvent/signal}} + Until all promises passed to {{AppHistoryNavigateEvent/respondWith()}} have settled + So that if the navigation is canceled, we can [=AbortSignal/signal abort=]. +
Any {{Promise}} that was returned + Until all promises passed to {{AppHistoryNavigateEvent/respondWith()}} have settled + 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); +const p2 = appHistory.navigate(url2); + + +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 {{AppHistory/navigate()}} happens. + +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 to-be-set serialized state, a [=serialized state=] 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 ongoing non-traverse navigation, which is an [=app history API navigation=] or null, initially null. + +Each {{AppHistory}} object has an associated ongoing traverse navigations, which is a [=map=] from strings to [=app history API navigations=], initially empty. + +Each {{AppHistory}} object has an associated post-navigate event ongoing navigation signal, which is an {{AbortSignal}} or null, initially null. + +An app history API navigation is a [=struct=] with the following [=struct/items=]: + +* An info, a JavaScript value +* A returned promise, a {{Promise}} +* A cleanup step, an algorithm step + +

We need to store the {{AbortSignal}} 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. So, we store it some of the time in the [=AppHistory/ongoing navigate event=]'s {{AppHistoryNavigateEvent/signal}} property, and the rest of the time in the [=AppHistory/post-navigate event ongoing navigation signal=]. + +

+ To set the upcoming non-traverse navigation given an {{AppHistory}} |appHistory| and a JavaScript value |info|: + + 1. Let |promise| be [=a new promise=] created in |appHistory|'s [=relevant Realm=]. + + 1. Let |cleanupStep| be an algorithm step which sets |appHistory|'s [=AppHistory/ongoing non-traverse navigation=] to null. + + 1. Let |ongoingNavigation| be an [=app history API navigation=] whose [=app history API navigation/info=] is |info|, [=app history API navigation/returned promise=] is |promise|, and [=app history API navigation/cleanup step=] is |cleanupStep|. + + 1. Assert: |appHistory|'s [=AppHistory/upcoming non-traverse navigation=] is null. + + 1. Set |appHistory|'s [=AppHistory/upcoming non-traverse navigation=] to |ongoingNavigation|. + + 1. Return |ongoingNavigation|. +
+ +
+ To promote the upcoming non-traverse navigation to ongoing given an {{AppHistory}} |appHistory|: + + 1. Assert: |appHistory|'s [=AppHistory/ongoing non-traverse navigation=] is null. + + 1. Set |appHistory|'s [=AppHistory/ongoing non-traverse navigation=] to |appHistory|'s [=AppHistory/upcoming non-traverse navigation=]. + + 1. Set |appHistory|'s [=AppHistory/upcoming non-traverse navigation=] to null. +
+ +
+ To set an ongoing traverse navigation given an {{AppHistory}} |appHistory|, a string |key|, and a JavaScript value |info|: + + 1. Let |promise| be [=a new promise=] created in |appHistory|'s [=relevant Realm=]. + + 1. Let |cleanupStep| be an algorithm step which [=map/removes=] |appHistory|'s [=AppHistory/ongoing traverse navigations=][|key|]. + + 1. Let |ongoingNavigation| be an [=app history API navigation=] whose [=app history API navigation/info=] is |info|, [=app history API navigation/returned promise=] is |promise|, and [=app history API navigation/cleanup step=] is |cleanupStep|. + + 1. Set |appHistory|'s [=AppHistory/ongoing traverse navigations=][|key|] to |ongoingNavigation|. + + 1. Return |ongoingNavigation|. +
+
@@ -399,7 +531,7 @@ Each {{AppHistory}} object has an associated navigate meth 1. Let |historyHandling| be "`replace`" if |options|["{{AppHistoryNavigateOptions/replace}}"] is true; otherwise, "`default`". - 1. Return the result of [=performing an app history navigation=] given [=this=], |urlRecord|, |serializedState|, |navigateInfo|, and |historyHandling|. + 1. Return the result of [=performing a non-traverse app history navigation=] given [=this=], |urlRecord|, |serializedState|, |navigateInfo|, and |historyHandling|.
@@ -419,11 +551,11 @@ Each {{AppHistory}} object has an associated navigate meth 1. Let |navigateInfo| be |options|["{{AppHistoryNavigationOptions/navigateInfo}}"] if it exists; otherwise, undefined. - 1. Return the result of [=performing an app history navigation=] given [=this=], |urlRecord|, |serializedState|, |navigateInfo|, and "`reload`". + 1. Return the result of [=performing a non-traverse app history navigation=] given [=this=], |urlRecord|, |serializedState|, |navigateInfo|, and "`reload`".
- To perform an app history navigation given an {{AppHistory}} object |appHistory|, a [=URL=] |url|, a [=serialized state=]-or-null |serializedState|, a JavaScript value |navigateInfo|, and a history handling behavior |historyHandling|: + 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 |navigateInfo|, and a history handling behavior |historyHandling|: 1. Let |browsingContext| be |appHistory|'s [=relevant global object=]'s [=Window/browsing context=]. @@ -431,27 +563,174 @@ Each {{AppHistory}} object has an associated navigate meth 1. Assert: |historyHandling| is either "`replace`", "`reload`", or "`default`". - 1. Let |promise| be [=a new promise=] created in |appHistory|'s [=relevant Realm=]. + 1. Let |ongoingNavigation| be the result of [=AppHistory/setting the upcoming non-traverse navigation=] for |appHistory| given |navigateInfo|. + + 1. Set |appHistory|'s [=AppHistory/to-be-set serialized state=] to |serializedState|. - 1. Let |previousPromise| be |appHistory|'s [=AppHistory/navigate method call promise=]. + 1. Navigate |browsingContext| to |url| with [=navigate/historyHandling=] set to |historyHandling|, [=navigate/appHistoryState=] set to |serializedState|, and the source browsing context set to |browsingContext|. - 1. Set |appHistory|'s [=AppHistory/navigate method call promise=] to |promise|. + 1. If |appHistory|'s [=AppHistory/upcoming non-traverse navigation=] is |ongoingNavigation|, then: - 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to |serializedState|. +

This means the navigate algorithm bailed out before ever getting to the [=inner navigate event firing algorithm=] which would [=AppHistory/promote the upcoming non-traverse navigation to ongoing=]. - 1. Navigate |browsingContext| to |url| with [=navigate/historyHandling=] set to |historyHandling|, [=navigate/appHistoryInfo=] set to |navigateInfo|, [=navigate/appHistoryState=] set to |serializedState|, and the source browsing context set to |browsingContext|. + 1. [=Reject=] |ongoingNavigation|'s [=app history API navigation/returned promise=] with a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=]. - 1. If |appHistory|'s [=AppHistory/navigate method call serialized state=] is non-null, then set |browsingContext|'s [=session history=]'s [=session history/current entry=]'s [=session history entry/app history state=] to |appHistory|'s [=AppHistory/navigate method call serialized state=]. + 1. Perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. - 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. +

We don't [=finalize with an aborted navigation error=] since that algorithm only makes sense after the {{AppHistory/navigate}} event has fired. - 1. Set |appHistory|'s [=AppHistory/navigate method call promise=] to |previousPromise|. + 1. If |appHistory|'s [=AppHistory/to-be-set serialized state=] is non-null, then set |browsingContext|'s [=session history=]'s [=session history/current entry=]'s [=session history entry/app history state=] to |appHistory|'s [=AppHistory/to-be-set serialized state=]. - 1. Return |promise|. + 1. Set |appHistory|'s [=AppHistory/to-be-set serialized state=] to null. + + 1. Return |ongoingNavigation|'s [=app history API navigation/returned promise=].

Unlike {{Location/assign()|location.assign()}} and friends, which are exposed across [=same origin-domain|origin-domain=] boundaries, {{AppHistory/navigate()|appHistory.navigate()}} and {{AppHistory/reload()|appHistory.reload()}} can only be accessed by code with direct synchronous access to the {{Window/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 [=navigate/exceptionsEnabled=] flag. We just treat all navigations as being initiated by the {{AppHistory}} object itself. +

Traversing

+ +
+
await {{Window/appHistory}} . {{AppHistory/goTo(key)|goTo}}(key) +
await {{Window/appHistory}} . {{AppHistory/goTo(key, options)|goTo}}(key, { {{AppHistoryNavigationOptions/navigateInfo}} }) +
+

Traverses the joint session history to the closest joint session history entry that matches the {{AppHistoryEntry}} with the given key. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event. + +

If a traversal to that joint session history is already in progress, then this will return a promise for that original traversal, and {{AppHistoryNavigationOptions/navigateInfo}} will be ignored. + +

The returned promise will behave as follows: + + * If there is no {{AppHistoryEntry}} in {{AppHistory/entries|appHistory.entries}} with the given key, it will reject with an "{{InvalidStateError}}" {{DOMException}}. + * For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}. + * For non-intercepted same-document traversals, it will fulfill after the traversal completes. + * For cross-document traversals, it will never settle. +

+ +
await {{Window/appHistory}} . {{AppHistory/back()|back}}() +
await {{Window/appHistory}} . {{AppHistory/back(options)|back}}({ {{AppHistoryNavigationOptions/navigateInfo}} }) +
+

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|appHistory.current}} updating. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event. + +

If a traversal to that joint session history is already in progress, then this will return a promise for that original traversal, and {{AppHistoryNavigationOptions/navigateInfo}} will be ignored. + +

The returned promise will behave as follows: + + * If {{AppHistory/canGoBack|appHistory.canGoBack}} is false, it will reject with an "{{InvalidStateError}}" {{DOMException}}. + * For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}. + * For non-intercepted same-document traversals, it will fulfill after the traversal completes. + * For cross-document traversals, it will never settle. +

+ +
await {{Window/appHistory}} . {{AppHistory/forward()|forward}}() +
await {{Window/appHistory}} . {{AppHistory/forward(options)|forward}}({ {{AppHistoryNavigationOptions/navigateInfo}} }) +
+

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|appHistory.current}} updating. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event. + +

If a traversal to that joint session history is already in progress, then this will return a promise for that original traversal, and {{AppHistoryNavigationOptions/navigateInfo}} will be ignored. + +

The returned promise will behave as follows: + + * If {{AppHistory/canGoBack|appHistory.canGoForward}} is false, it will reject with an "{{InvalidStateError}}" {{DOMException}}. + * For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}. + * For non-intercepted same-document traversals, it will fulfill after the traversal completes. + * For cross-document traversals, it will never settle. +

+
+ +
+ The goTo(|key|, |options|) method steps are: + + 1. If [=this=]'s [=AppHistory/current index=] is −1, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. If [=this=]'s [=AppHistory/entry list=] does not contain any {{AppHistoryEntry}} whose [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=] equals |key|, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. Return the result of [=performing an app history traversal=] given [=this=], |key|, and |options|. +
+ +
+ The back(|options|) method steps are: + + 1. If [=this=]'s [=AppHistory/current index=] is −1 or 0, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. Let |key| be [=this=]'s [=AppHistory/entry list=][[=this=]'s [=AppHistory/current index=] − 1]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=]. + + 1. Return the result of [=performing an app history traversal=] given [=this=], |key|, and |options|. +
+ +
+ The forward(|options|) method steps are: + + 1. If [=this=]'s [=AppHistory/current index=] is −1 or is equal to [=this=]'s [=AppHistory/entry list=]'s [=list/size=] − 1, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. Let |key| be [=this=]'s [=AppHistory/entry list=][[=this=]'s [=AppHistory/current index=] + 1]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=]. + + 1. 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 [=Document/fully active=], then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. If |appHistory|'s [=relevant global object=]'s [=associated Document=]'s unload counter is greater than 0, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. + + 1. If |appHistory|'s [=AppHistory/entry list=][|appHistory|'s [=AppHistory/current index=]]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=] equals |key|, then return [=a promise resolved with=] undefined. + + 1. Let |navigable| be |appHistory|'s [=relevant global object=]'s [=Window/browsing context=]'s [=browsing context/containing navigable=]. + + 1. Let |traversable| be |navigable|'s [=navigable/traversable navigable=]. + + 1. Let |initiatorBC| be |appHistory|'s [=relevant global object=]'s [=Window/browsing context=]. + + 1. Let |info| be |options|["{{AppHistoryNavigationOptions/navigateInfo}}"] if it [=map/exists=], or undefined otherwise. + + 1. If |appHistory|'s [=AppHistory/ongoing traverse navigations=][|key|] [=map/exists=], then return |appHistory|'s [=AppHistory/ongoing traverse navigations=][|key|]'s [=app history API navigation/returned promise=]. + + 1. Let |ongoingNavigation| be the result of [=AppHistory/setting an ongoing traverse navigation=] for |appHistory| given |key| and |info|. + + 1. [=parallel queue/Enqueue the following steps=] on |traversable|'s [=traversable navigable/session history traversal queue=]: + + 1. Let |navigableEntries| be the result of [=navigable/getting the session history entries=] given |navigable|. + + 1. Let |targetEntry| be the [=session history entry=] in |navigableEntries| whose [=session history entry/app history key=] equals |key|. If no such entry exists, then: + + 1. [=Reject=] |ongoingNavigation|'s [=app history API navigation/returned promise=] with an "{{InvalidStateError}}" {{DOMException}}. + + 1. Perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. + + 1. 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). + + 1. If |targetEntry| is |navigable|'s [=navigable/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. + + 1. Let |targetStep| be null. + + 1. If |targetEntry|'s [=session history entry/step=] is greater than |traversable|'s [=traversable navigable/current session history step=], then set |targetStep| to |targetEntry|'s [=session history entry/step=]. + + 1. Otherwise: + + 1. Let |afterTarget| be the [=session history entry=] after |targetEntry| in |navigableEntries|. + + 1. Let |allSteps| be the result of [=traversable navigable/getting all history steps=] that are part of the target session TODO. + + 1. Set |targetStep| to the greatest number in |allSteps| that is less than |afterTarget|'s [=session history entry/step=]. + + 1. [=Apply the history step=] |targetStep| to |traversable|, with true, |initiatorBC|, and "[=user navigation involvement/none=]". + + - If this aborts due to user-canceled unloading or due to the {{AppHistory/navigate}} event being canceled, then [=finalize with an aborted navigation error=] given |appHistory| and |ongoingNavigation|. + + - If this aborts due to the initiator allowed-to-navigate check, then [=finalize with an aborted navigation error=] given |appHistory|, |ongoingNavigation|, and a [=new=] "{{SecurityError}}" {{DOMException}} created in |appHistory|'s [=relevant Realm=]. + +

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

+ + 1. Return |ongoingNavigation|'s [=app history API navigation/returned promise=]. +
+

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: @@ -673,7 +952,7 @@ The sameDocument getter steps a
- To fire a traversal `navigate` event at an {{AppHistory}} |appHistory| given a [=session history entry=] |destinationEntry|, an optional [=user navigation involvement=] |userInvolvement| (default "[=user navigation involvement/none=]"), and an optional JavaScript value |info| (default undefined): + To fire a traversal `navigate` event at an {{AppHistory}} |appHistory| given a [=session history entry=] |destinationEntry|, and an optional [=user navigation involvement=] |userInvolvement| (default "[=user navigation involvement/none=]"): 1. Let |event| be the result of [=creating an event=] given {{AppHistoryNavigateEvent}}, in |appHistory|'s [=relevant Realm=]. 1. Set |event|'s [=AppHistoryNavigateEvent/destination entry=] to |destinationEntry|. @@ -690,11 +969,11 @@ The sameDocument getter steps a 1. Set |destination|'s [=AppHistoryDestination/index=] to −1. 1. Set |destination|'s [=AppHistoryDestination/state=] to null. 1. Set |destination|'s [=AppHistoryDestination/is same document=] to true if |destinationEntry|'s [=session history entry/document=] is equal to |appHistory|'s [=relevant global object=]'s [=associated Document=]; otherwise false. - 1. Return the result of performing the [=inner navigate event firing algorithm=] given |appHistory|, "{{AppHistoryNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, |info|, and null. + 1. Return the result of performing the [=inner navigate event firing algorithm=] given |appHistory|, "{{AppHistoryNavigationType/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 "[=user navigation involvement/none=]"), and an optional value |info| (default undefined), an optional [=serialized state=]-or-null |state| (default null), an optional [=list=] of {{FormData}} [=FormData/entries=] or null |formDataEntryList| (default null), and an optional [=serialized state=]-or-null |classicHistoryAPISerializedData| (default 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 "[=user navigation involvement/none=]"), an optional [=serialized state=]-or-null |state| (default null), an optional [=list=] of {{FormData}} [=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=]. 1. Set |event|'s [=AppHistoryNavigateEvent/classic history API serialized data=] to |classicHistoryAPISerializedData|. @@ -705,27 +984,36 @@ The sameDocument getter steps a 1. Set |destination|'s [=AppHistoryDestination/index=] to −1. 1. Set |destination|'s [=AppHistoryDestination/state=] to |state|. 1. Set |destination|'s [=AppHistoryDestination/is same document=] to |isSameDocument|. - 1. Return the result of performing the [=inner navigate event firing algorithm=] given |appHistory|, |navigationType|, |event|, |destination|, |userInvolvement|, |info|, and |formDataEntryList|. + 1. 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|, a JavaScript value |info|, and a [=list=] of {{FormData}} [=FormData/entries=] or null |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}} [=FormData/entries=] or null |formDataEntryList|: - 1. If |appHistory|'s [=relevant global object=]'s [=Window/browsing context=] is still on its initial `about:blank` `Document`, then return true. + 1. [=AppHistory/Promote the upcoming non-traverse navigation to ongoing=] given |appHistory|. + 1. Let |ongoingNavigation| be null. + 1. If |destination|'s [=AppHistoryDestination/key=] is null, then set |ongoingNavigation| to |appHistory|'s [=AppHistory/ongoing non-traverse navigation=]. + 1. Otherwise, if |appHistory|'s [=AppHistory/ongoing traverse navigations=][|destination|'s [=AppHistoryDestination/key=]] [=map/exists=], then set |ongoingNavigation| to |appHistory|'s [=AppHistory/ongoing traverse navigations=][|destination|'s [=AppHistoryDestination/key=]]. + 1. Let |currentURL| be |appHistory|'s [=relevant global object=]'s [=associated document=]'s [=Document/URL=]. + 1. If |destination|'s [=AppHistoryDestination/URL=] is [=rewritable=] relative to |currentURL|, and either |destination|'s [=AppHistoryDestination/is same document=] is true or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{AppHistoryNavigateEvent/canRespond}} to true. Otherwise, initialize it to false. + 1. If either |userInvolvement| is not "[=user navigation involvement/browser UI=]" or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true. Otherwise, initialize it to false. + 1. If either |appHistory|'s [=relevant global object=]'s [=Window/browsing context=] is still on its initial `about:blank` `Document`, or both |event|'s {{AppHistoryNavigateEvent/canRespond}} and |event|'s {{Event/cancelable}} are false, then: + 1. If |ongoingNavigation| is not null, then: + 1. [=Resolve=] |ongoingNavigation|'s [=app history API navigation/returned promise=]. + 1. Perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. + 1. Return true. 1. Initialize |event|'s {{Event/type}} to "{{AppHistory/navigate}}". 1. Initialize |event|'s {{AppHistoryNavigateEvent/navigationType}} to |navigationType|. - 1. Initialize |event|'s {{AppHistoryNavigateEvent/info}} to |info|. 1. Initialize |event|'s {{AppHistoryNavigateEvent/destination}} to |destination|. - 1. Let |currentURL| be |appHistory|'s [=relevant global object=]'s [=associated document=]'s [=Document/URL=]. + 1. If |ongoingNavigation| is not null, then initialize |event|'s {{AppHistoryNavigateEvent/info}} to |ongoingNavigation|'s [=app history API navigation/info=]. Otherwise, initialize it to undefined. +

At this point |ongoingNavigation|'s [=app history API navigation/info=] is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the [=app history API navigation=]. + 1. Initialize |event|'s {{AppHistoryNavigateEvent/signal}} to a [=new=] {{AbortSignal}} created in |appHistory|'s [=relevant Realm=]. 1. If all of the following are true: * |destination|'s [=AppHistoryDestination/is same document=] is true; * |destination|'s [=AppHistoryDestination/URL=] [=url/equals=] |currentURL| with [=url/equals/exclude fragments=] set to true; and * |destination|'s [=AppHistoryDestination/URL=]'s [=url/fragment=] is not [=string/is|identical to=] |currentURL|'s [=url/fragment=] then initialize |event|'s {{AppHistoryNavigateEvent/hashChange}} to true. Otherwise, initialize it to false. - 1. If |destination|'s [=AppHistoryDestination/URL=] is [=rewritable=] relative to |currentURL|, and either |destination|'s [=AppHistoryDestination/is same document=] is true or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{AppHistoryNavigateEvent/canRespond}} to true. Otherwise, initialize it to false. - 1. If either |userInvolvement| is not "[=user navigation involvement/browser UI=]" or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true. - 1. If both |event|'s {{AppHistoryNavigateEvent/canRespond}} and |event|'s {{Event/cancelable}} are false, then return true. (No event is actually fired.) 1. If |userInvolvement| is "[=user navigation involvement/none=]", then initialize |event|'s {{AppHistoryNavigateEvent/userInitiated}} to false. Otherwise, initialize it to true. 1. If |formDataEntryList| is not null, then initialize |event|'s {{AppHistoryNavigateEvent/formData}} to a [=new=] {{FormData}} created in |appHistory|'s [=relevant Realm=], associated to |formDataEntryList|. Otherwise, initialize it to null. 1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null. @@ -734,57 +1022,61 @@ The sameDocument getter steps a 1. Let |shouldContinue| be |dispatchResult|. 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null. 1. If |appHistory|'s [=relevant global object=]'s [=active Document=] is not [=Document/fully active=], then: - 1. [=Synchronously finalize with an aborted navigation error=] given |appHistory| and |event|'s {{AppHistoryNavigateEvent/signal}}. + 1. [=Finalize with an aborted navigation error=] given |appHistory| and |ongoingNavigation|. 1. Return false. -

This can occur if an event listener disconnected the <{iframe}> corresponding to [=this=]'s [=relevant global object=].

+

This can occur if an event listener disconnected the <{iframe}> corresponding to [=this=]'s [=relevant global object=]. 1. If |dispatchResult| is true: 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty and |navigationType| is not "{{AppHistoryNavigationType/traverse}}": 1. Let |isPush| be true if |navigationType| is "{{AppHistoryNavigationType/push}}"; otherwise, false. 1. Run the URL and history update steps given |event|'s [=relevant global object=]'s [=associated document=] and |event|'s {{AppHistoryNavigateEvent/destination}}'s [=AppHistoryDestination/URL=], with [=URL and history update steps/serializedData=] set to |event|'s [=AppHistoryNavigateEvent/classic history API serialized data=] and [=URL and history update steps/isPush=] set to |isPush|. 1. Set |shouldContinue| to false. - 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty or |destination|'s [=AppHistoryDestination/is same document=] is true, then: - 1. Let |navigateMethodCallPromise| be |appHistory|'s [=AppHistory/navigate method call promise=]. - 1. [=Wait for all=] of |event|'s [=AppHistoryNavigateEvent/navigation action promises list=], with the following success steps: - 1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|. - 1. If |navigateMethodCallPromise| is non-null, then [=resolve=] |navigateMethodCallPromise| with undefined. - and the following failure steps given reason |rejectionReason|: - 1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |rejectionReason|, and {{ErrorEvent/message}}, {{ErrorEvent/filename}}, {{ErrorEvent/lineno}}, and {{ErrorEvent/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. - 1. If |navigateMethodCallPromise| is non-null, then [=reject=] |navigateMethodCallPromise| with |rejectionReason|. - -

If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is non-empty, then {{AppHistoryNavigateEvent/respondWith()}} was called and so we're performing a same-document navigation, for which we want to fire {{AppHistory/navigatesuccess}} or {{AppHistory/navigateerror}} events, and resolve or reject the promise returned by the corresponding {{AppHistory/navigate()|appHistory.navigate()}} call if one exists. Otherwise, if the navigation is same-document and was not canceled, we still perform these actions after a microtask, treating them as an instantly-successful navigation. + 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty or |destination|'s [=AppHistoryDestination/is same document=] is true, then [=wait for all=] of |event|'s [=AppHistoryNavigateEvent/navigation action promises list=], with the following success steps: + 1. If |event|'s {{AppHistoryNavigateEvent/signal}}'s [=AbortSignal/aborted flag=] is set, then abort these steps. + 1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|. + 1. If |ongoingNavigation| is non-null, then: + 1. [=Resolve=] |ongoingNavigation|'s [=app history API navigation/returned promise=] with undefined. + 1. Perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. + and the following failure steps given reason |rejectionReason|: + 1. If |event|'s {{AppHistoryNavigateEvent/signal}}'s [=AbortSignal/aborted flag=] is set, then abort these steps. + 1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |rejectionReason|, and {{ErrorEvent/message}}, {{ErrorEvent/filename}}, {{ErrorEvent/lineno}}, and {{ErrorEvent/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. + 1. If |ongoingNavigation| is non-null, then: + 1. [=Reject=] |ongoingNavigation|'s [=app history API navigation/returned promise=] with |rejectionReason|. + 1. Perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. + +

If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is non-empty, then {{AppHistoryNavigateEvent/respondWith()}} was called and so we're performing a same-document navigation, for which we want to fire {{AppHistory/navigatesuccess}} or {{AppHistory/navigateerror}} events, and resolve or reject the promise returned by the corresponding {{AppHistory/navigate()|appHistory.navigate()}} call if one exists. Otherwise, if the navigation is same-document and was not canceled, we still perform these actions after a microtask (by waiting for zero promises), or after the appropriate task in the traversal case. 1. Otherwise: - 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. + 1. Set |appHistory|'s [=AppHistory/to-be-set serialized state=] to null.

This ensures that any call to {{AppHistory/navigate()|appHistory.navigate()}} which triggered this algorithm does not overwrite the [=session history entry/app history state=] of the [=session history/current entry=] for cross-document navigations. - 1. Otherwise, [=synchronously finalize with an aborted navigation error=] given |appHistory| and |event|'s {{AppHistoryNavigateEvent/signal}}. + 1. Otherwise, |navigationType| is not "{{AppHistoryNavigationType/traverse}}", [=finalize with an aborted navigation error=] given |appHistory| and |ongoingNavigation|. +

If |navigationType| is "{{AppHistoryNavigationType/traverse}}", then we will [=finalize with an aborted navigation error=] in [=perform an app history traversal=]. 1. Return |shouldContinue|.

- To synchronously finalize with an aborted navigation error given an {{AppHistory}} |appHistory| and an {{AbortSignal}} |signal|: + 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. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. + 1. Let |signal| be null. + 1. If |appHistory|'s [=AppHistory/ongoing navigate event=] is non-null, then: + 1. Set |signal| to the value of |appHistory|'s [=AppHistory/ongoing navigate event=]'s {{AppHistoryNavigateEvent/signal}} property. + 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=Event/canceled flag=] to true. + 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null. + 1. Otherwise: + 1. Assert: |appHistory|'s [=AppHistory/post-navigate event ongoing navigation signal=] is not null. + 1. Set |signal| to |appHistory|'s [=AppHistory/post-navigate event ongoing navigation signal=]. + 1. Set |appHistory|'s [=AppHistory/post-navigate event ongoing navigation signal=] to null. + 1. Set |appHistory|'s [=AppHistory/to-be-set serialized state=] to null.

This ensures that any call to {{AppHistory/navigate()|appHistory.navigate()}} which triggered this algorithm does not overwrite the [=session history entry/app history state=] of the [=session history/current entry=] for aborted navigations. + 1. Let |promise| be null. + 1. If |ongoingNavigation| is non-null, then set |promise| to |ongoingNavigation|'s [=app history API navigation/returned promise=]. + 1. If |ongoingNavigation| is non-null, then perform |ongoingNavigation|'s [=app history API navigation/cleanup step=]. 1. [=AbortSignal/Signal abort=] on |signal|. - -

This might do nothing, if |signal| was previously aborted by cancel any ongoing `navigate` event.

1. [=Queue a microtask=] on |appHistory|'s [=relevant agent=]'s [=agent/event loop=] to perform the following steps: - 1. Let |error| be a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=]. + 1. If |error| was not given, then set |error| to a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=]. 1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |error|, {{ErrorEvent/message}} initialized to the value of |error|'s {{DOMException/message}} property, {{ErrorEvent/filename}} initialized to the empty string, and {{ErrorEvent/lineno}} and {{ErrorEvent/colno}} initialized to 0. - 1. If |appHistory|'s [=AppHistory/navigate method call promise=] is non-null, then [=reject=] |appHistory|'s [=AppHistory/navigate method call promise=] with |error|. -
- -
- To cancel any ongoing `navigate` event for an {{AppHistory}} |appHistory|: - - 1. If |appHistory|'s [=AppHistory/ongoing navigate event=] is non-null, then: - 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=Event/canceled flag=] to true. - 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null. - 1. [=AbortSignal/Signal abort=] on |appHistory|'s [=AppHistory/ongoing navigate event=]'s {{AppHistoryNavigateEvent/signal}}. - -

Because this cancels the event, this will eventually lead to the inner `navigate` event firing algorithm calling [=synchronously finalize with an aborted navigation error=] once the event firing process completes. However, we want to [=AbortSignal/signal abort=] on the relevant {{AppHistoryNavigateEvent/signal|event.signal}} right away. + 1. If |promise| is non-null, then [=reject=] it with |error|.

@@ -825,8 +1117,7 @@ interface AppHistoryEntry : EventTarget {

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)`. +

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

entry . {{AppHistoryEntry/id}} @@ -1008,7 +1299,6 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap 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 [=Window/app history=]. - 1. [=Cancel any ongoing navigate event=] for |appHistory|. 1. Let |navigationType| be "{{AppHistoryNavigationType/push}}" if isPush is true, and "{{AppHistoryNavigationType/replace}}" otherwise. 1. Let |continue| be the result of [=firing a push or replace navigate event=] at |appHistory| with [=fire a push or replace navigate event/navigationType=] set to |navigationType|, [=fire a push or replace navigate event/isSameDocument=] set to true, [=fire a push or replace navigate event/destinationURL=] set to newURL, and [=fire a push or replace navigate event/classicHistoryAPISerializedData=] set to serializedData. 1. If |continue| is false, return. @@ -1024,10 +1314,9 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap