Skip to content

Commit

Permalink
Add appHistoryEntry.id and clarify object identity
Browse files Browse the repository at this point in the history
Closes #7.
  • Loading branch information
domenic authored Apr 2, 2021
1 parent 3a108e2 commit d13d48b
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 12 deletions.
75 changes: 64 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ appHistory.addEventListener("currentchange", e => {
- [The current entry](#the-current-entry)
- [Inspection of the app history list](#inspection-of-the-app-history-list)
- [Navigation through the app history list](#navigation-through-the-app-history-list)
- [Keys and IDs](#keys-and-ids)
- [Navigation monitoring and interception](#navigation-monitoring-and-interception)
- [Example: replacing navigations with single-page app navigations](#example-replacing-navigations-with-single-page-app-navigations)
- [Example: async transitions with special back/forward handling](#example-async-transitions-with-special-backforward-handling)
Expand All @@ -78,7 +79,7 @@ appHistory.addEventListener("currentchange", e => {
- [Example: single-page app "redirects"](#example-single-page-app-redirects)
- [Example: cross-origin affiliate links](#example-cross-origin-affiliate-links)
- [Aborted navigations](#aborted-navigations)
- [New navigation APIs](#new-navigation-apis)
- [New navigation API](#new-navigation-api)
- [Example: using `navigateInfo`](#example-using-navigateinfo)
- [Example: next/previous buttons](#example-nextprevious-buttons)
- [Per-entry events](#per-entry-events)
Expand Down Expand Up @@ -179,7 +180,9 @@ Additionally, we hope to drive interoperability through tests, spec updates, and
The entry point for the app history API is `window.appHistory`. Let's start with `appHistory.current`, which is an instance of the new `AppHistoryEntry` class. This class has the following readonly properties:
- `key`: a user-agent-generated UUID identifying this history entry. In the past, applications have used the URL as such a key, but the URL is not guaranteed to be unique.
- `id`: a user-agent-generated UUID identifying this particular `AppHistoryEntry`. This will be changed upon any mutation of the current app history entry, such as replacing its state or updating the current URL.
- `key`: a user-agent-generated UUID identifying this history entry "slot". This will stay the same even if the entry is replaced.
- `index`: the index of this `AppHistoryEntry` within the app history list. (Or, `-1` if the entry is no longer in the list, or not yet in the list.)
Expand All @@ -201,15 +204,32 @@ console.assert(appHistory.current.getState().test === 2);
appHistory.navigate({ state: { ...appHistory.current.getState(), test: 3 });
```
Crucially, `appHistory.current` stays the same regardless of what iframe navigations happen. It only reflects the current entry for the current frame. The complete list of ways the current app history entry can change are:
Crucially, `appHistory.current` stays the same regardless of what iframe navigations happen. It only reflects the current entry for the current frame. The complete list of ways the current app history entry can change to a new entry (with a new `AppHistoryEntry` object, and a new `key` value) are:
- A fragment navigation, which will copy over the app history state.
- A fragment navigation, which will copy over the app history state to the new entry.
- Via the same-document navigation API `history.pushState()`. (Not `history.replaceState()`.)
- A full-page navigation to a different document. This could be an existing document in the browser's back/forward cache, or a new document. In the latter case, this will generate a new entry on the new page's `window.appHistory` object, somewhat similar to `appHistory.navigate(navigatedToURL, { state: null })`. Note that if the navigation is cross-origin, then we'll end up in a separate app history list for that other origin.
- When using the `navigate` event to [convert a cross-document navigation into a same-document navigation](#navigation-monitoring-and-interception).
- When using the `navigate` event to [convert a cross-document non-replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
The current entry can be replaced with a new entry, with a new `AppHistoryEntry` object and a new `id` (but usually the same `key`), in the following ways:
- Via the same-document navigation API `history.replaceState()`.
- Via cross-document replace navigations generated by `location.replace()` or `appHistory.navigate({ replace: true, ... })`. Note that if the navigation is cross-origin, then we'll end up in a separate app history list for that other origin, where `key` will not be preserved.
- When using the `navigate` event to [convert a cross-document replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
In both cases, for same-document navigations a `currentchange` event will fire on `appHistory`:
```js
appHistory.addEventListener("currentchange", () => {
// appHistory.current has changed: either to a completely new entry (with new key),
// or it has been replaced (keeping the same key).
});
```
### Inspection of the app history list
Expand All @@ -223,7 +243,22 @@ In combination with the following section, the `entries` API also allows applica
The way for an application to navigate through the app history list is using `appHistory.goTo(key)`. For example:
_TODO: realistic example of when you'd use this._
```js
function renderHomepage() {
const homepageKey = appHistory.current.key;

// ... set up some UI ...

document.querySelector("#home-button").addEventListener("click", async e => {
try {
await appHistory.goTo(homepageKey);
} catch {
// Fall back to a normal "forward" navigation
appHistory.navigate("/");
}
});
}
```
Unlike the existing history API's `history.go()` method, which navigates by offset, navigating by key allows the application to not care about intermediate history entries; it just specifies its desired destination entry. There are also convenience methods, `appHistory.back()` and `appHistory.forward()`, and convenience booleans, `appHistory.canGoBack` and `appHistory.canGoForward`.
Expand All @@ -241,6 +276,22 @@ All of these methods return promises, because navigations can be intercepted and
As discussed in more detail in the section on [integration with the existing history API and spec](#integration-with-the-existing-history-api-and-spec), navigating through the app history list does navigate through the joint session history. This means it _can_ impact other frames on the page. It's just that, unlike `history.back()` and friends, such other-frame navigations always happen as a side effect of navigating your own frame; they are never the sole result of an app history traversal.
### Keys and IDs
As noted [above](#the-current-entry), `key` stays stable to represent the "slot" in the history list, whereas `id` gets updated whenever the app history entry is updated. This allows them to serve distinct purposes:
- `key` provides a stable identifier for a given slot in the app history entry list, for use by the `appHistory.goTo()` method which allows navigating to specific waypoints within the history list.
- `id` provides an identifier for the specific URL and app history state currently in the entry, which can be used to correlate an app history entry with an out-of-band resource such as a cache.
With the `window.history` API, web applications have tried to use the URL for such purposes, but the URL is not guaranteed to be unique within a given history list.
Note that both `key` and `id` are user-agent-generated random UUIDs. This is done, instead of e.g. using a numeric index, to encourage treating them as opaque identifiers.
Both `key` and `id` are stored in the browser's session history, and as such are stable across session restores.
Note that `key` is not a stable identifier for a slot in the _joint session history list_, but instead in the _app history list_. In particular, this means that if a given history entry is replaced with a cross-origin one, which lives in a different app history list, it will get a new key. (This replacement prevents cross-site tracking.)
### Navigation monitoring and interception
The most interesting event on `window.appHistory` is the one which allows monitoring and interception of navigations: the `navigate` event. It fires on almost any navigation, either user-initiated or application-initiated, which would update the value of `appHistory.current`. This includes cross-origin navigations (which will take us out of the current app history list); see [below](#example-cross-origin-affiliate-links) for an example of how this is useful. **We expect this to be the main event used by application- or framework-level routers.**
Expand Down Expand Up @@ -736,7 +787,7 @@ async function showPhoto(photoId) {
}
```

Note how in the event handler for these events, `appHistory.current` will be set as expected (and equal to `e.target`), so that the event handler can use its properties and methods (like `key`, `url`, or `getState()`) as needed.
Note how in the event handler for these events, `appHistory.current` will be set as expected (and equal to `e.target`), so that the event handler can use its properties and methods (like `id`, `url`, or `getState()`) as needed.

Finally, there's a `dispose` event, which occurs when an app history entry is permanently evicted and unreachable: for example, in the following scenario.
Expand Down Expand Up @@ -978,9 +1029,10 @@ Example: if a browsing session contains session history entries with the URLs
then, if the current entry is 4, there would only be one `AppHistoryEntry` in `appHistory.entries`, corresponding to 4 itself. If the current entry is 2, then there would be two `AppHistoryEntries` in `appHistory.entries`, corresponding to 1 and 2.
To make this correspondence work, every spec-level session history entry would gain two new fields:
To make this correspondence work, every spec-level session history entry would gain three new fields:
- key, containing a browser-generated UUID. This is what backs `appHistoryEntry.key`.
- id, containing a browser-generated UUID. This is what backs `appHistoryEntry.id`.
- app history state, containing a JavaScript value. This is what backs `appHistoryEntry.getState()`.
Note that the "app history state" field has no interaction with the existing "serialized state" field, which is what backs `history.state`. This route was chosen for a few reasons:
Expand Down Expand Up @@ -1062,11 +1114,11 @@ Finally, note that user agents can continue to refine their mapping of UI to joi
Privacy-wise, this feature is neutral, due to its strict same-origin contiguous entry scoping. That is, it only exposes information which the application already has access to, just in a more convenient form. The storage of app history state in the `AppHistoryEntry`s is a convenience with no new privacy concerns, since that state is only accessible same-origin; that is, it provides the same power as something like `sessionStorage` or `history.state`.
One particular point of interest is the user-agent generated `appHistoryEntry.key` field, which is a user-agent-generated UUID. Here again the strict same-origin contiguous entry scoping prevents this from being used for cross-site tracking or similar. Specifically:
One particular point of interest is the user-agent generated `appHistoryEntry.key` and `appHistoryEntry.id` fields, which are a user-agent-generated random UUIDs. Here again the strict same-origin contiguous entry scoping prevents them from being used for cross-site tracking or similar. Specifically:
- This key lives only for the duration of that app history entry, i.e. for the lifetime of the browsing session. For example, opening a new tab (or iframe) to the same URL will generate a different `key` value. So it is not a stable user-specific identifier.
- These UUIDs lives only for the duration of that app history entry, which is at most for the lifetime of the browsing session. For example, opening a new tab (or iframe) to the same URL will generate different `key` and `id` values. So it is not a stable user-specific identifier.
- This information is not accessible across sites, as a given app history entry is specific to a frame and origin. That is, cross-site pages will always have different `key` values for all `AppHistoryEntry`s they can examine; there is no way to use app history entry `key`s to correlate users.
- This information is not accessible across sites, as a given app history entry is specific to a frame and origin. That is, cross-site pages will always have different `key` and `id` values for all `AppHistoryEntry`s they can examine; there is no way to use app history entry keys and IDs to correlate users.
(Collaborating cross-origin same-site pages can inspect each other's `AppHistoryEntry`s using `document.domain`, but they can also inspect every other aspect of each others' global objects.)
Expand Down Expand Up @@ -1206,6 +1258,7 @@ interface AppHistory : EventTarget {
[Exposed=Window]
interface AppHistoryEntry : EventTarget {
readonly attribute DOMString key;
readonly attribute DOMString id;
readonly attribute USVString url;
readonly attribute long long index;
readonly attribute boolean finished;
Expand Down
2 changes: 1 addition & 1 deletion security-privacy-questionnaire.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ So, this can't be used for spoofing the URL by, for example, responding to a nav

**What temporary identifiers do the features in this specification create or expose to the web?**

Each app history entry has an associated auto-generated `key` property, which is a UUID. We believe this is not problematic; see more discussion in [the main explainer](./README.md#security-and-privacy-considerations).
Each app history entry has associated auto-generated `key` and `id` properties, which are random UUIDs. We believe this is not problematic; see more discussion in [the main explainer](./README.md#security-and-privacy-considerations).

**How does this specification distinguish between behavior in first-party and third-party contexts?**

Expand Down

0 comments on commit d13d48b

Please sign in to comment.