': [
META, // metadata entry
RETABID, // 1st tab id reference
RETABID, // 2nd tab id reference
... // etc.
]
```
The first (zeroth) entry in the list (`META`) is an object containing metadata
for the window. Possible metadata properties are:
* `closed` (`DATETIME`)---Window is closed (unset for open windows, or set to
the `DATETIME` when the tab was closed). NOTA BENE: When a window is closed,
the tabs in that window are *not* set to `closed`---this is by design (so
that opened tabs can be restored when window is restored).
* `incognito` (Boolean)---If set, indicate the given window was/is in incognito
mode.
* `pos` (integer)---The currently selected tab.
There are also two special records called `'open'` and `'closed'` (literal
strings) which lists the `REWINID` numbers of the currently closed and
currently open windows, respectively.
```
'open' : [REWINID, REWINID, ...]
'closed': [REWINID, REWINID, ...]
```
(In the future, the literal `'open'` and `'closed'` may be followed by a
number, so that the first thousand [or whatever number] closed windows are
listed under `'closed'`, and the next set of windows under `'closed1'`
etc.---The idea being that the names be predictable so they may fetched without
having to scan the entirety of `storage.local`.)
### Mapping `windowId`
The following mapping table is used to convert the browser's numerical
`windowId` to the Rewin's alphanumeric `REWINID` (beginning with `'w'`) that is
used for `storage.local`.
These mapping values are only available for currently open windows (as closed
windows do not have a browser `windowId`), they are stored in a variable in
`rewin-background.js`, and are never saved to disk. On (browser or extension)
restart, and when previously closed tabs are reopened, a new mapping is
created from new browser `windowId` to the (unchanged) `REWINID`.
```
let winMap = {
WINDOWID: REWINID,
...
}
```
To get the current for a window with browser `windowId`:
```
browser.storage.local.get(winMap[windowId])
.then(onGet, onErr)
```
### Window Actions
| When | Change |
|---------------------------|------------------------------------------|
| Scan window's history | Create & populate window & tab record(s) |
| Window created | Create window tab, and add to `'open'` |
| Window closed | Move window tab, `'open'` → `'closed'` |
| Window restored | Move window tab, `'closed'` → `'open'` |
| Tab created | Add tab ID to window record |
| Tab moved within window | Change tab position in window record |
| Tab moved between windows | Remove from window, add in other window |
No other operations affect the window values. **Nota bene:** Closing a window
does not change any tab values, and closing a tab only updates the tab's
metadata (it does not affect the window). However, if a tab is dragged from one
window into another, it's removed from the original window.
## Fake History Entries
The current approach to restoring history entries involves faking them. This is
because the entries are created by means of `history.pushState()`, which
requires that each inserted history item share the same [origin] (that is, the
same *protocol* and *domain*) as page currently loaded when `pushState()` is
invoked.
As a workaround, these (fake) history items are given URL parameters which
specify the URL that should actually be loaded (`rewin_url`), and a random
string (`rewin`) which guarantees that each URL is unique (as `pushState()`
will fail to insert an URL that is already present elsewhere in the history).
When the user then navigates to one of the fake entries, the page is reloaded
using the correct URL. This document reload is performed by a content script,
and this content script keeps track of any remaining, not-yet-reloaded history
entries. As these history entries are visited they are removed from the
internal state of the content script, and if all of the fake entries are
visited, then content script quietly exits (its job done).
This info is recreated when a tab is restored, and cleared when the tab is
closed.
```
let histMap = {
TABID: {
FAKEID: URL,
...
}
}
```
In the future, history *might* be populated with `tabs.update()`, in which case
these fake history entries will not be needed.
[Continuous History Saving]: #continuous-history-saving
# Continuous History Saving
Rewin monitors user page navigation, and whenever a page is added or updated in
history (through navigation, or Javascript `replaceState()` or `pushState()`)
that page is recorded in Rewin's history as well.
Here we'll use the [webNavigation API] and its `onCommitted`,
`onHistoryStateUpdated` and `onReferenceFragmentUpdated` events to listen for
user navigation.
```
browser.webNavigation.onCommitted.addListener(evt => {
...
})
```
Also, we'll need to be able to detect when the user is navigating using history
by testing whether `webNavigation.TransitionQualifier` is `'forward_back'`.
## User Navigation Types
`webNavigation.TransitionQualifier` `"from_address_bar"` and
`webNavigation.TransitionType` `"typed"`.
-------------------------------------------------------------------------------
Difference between `webNavigation.TransitionQualifier === "from_address_bar"`
and `webNavigation.TransitionType === "typed"`.
* `TransitionType === 'typed'` indicates how navigation started. User *typed* a
URL directly into the address bar.
* `TransitionQualifier === 'from_address_bar'` indicates extra detail.
Navigation originated *from* the address bar, regardless of how (typed,
search, or history dropdown).
**Key Takeaway:** If it's `typed`, it's also `from_address_bar`. But if it's
`from_address_bar`, it's not necessarily `typed` (could be a search, or
selecting a history item from the address bar dropdown).
-------------------------------------------------------------------------------
When `TransitionQualifier === 'from_address_bar'` the following
`TransitionType` values are possible in the `webNavigation.onCommitted` event:
The key is that `from_address_bar` is a **qualifier** that specifies *where the
navigation originated*, while `transitionType` tells you *what kind of
navigation* it was.
* `typed`: This is the most common and direct combination. The user explicitly
typed a URL into the address bar and pressed Enter, or selected a direct URL
suggestion from the address bar's dropdown.
* `generated`: This occurs when the user starts typing in the address bar, and
then clicks on a suggested entry that is **not a direct URL**. This often
happens with search queries (e.g., typing "weather" and then the browser
generates a search URL like `google.com/search?q=weather`). Even though a
search query was entered, the navigation originated *from* the address bar.
* `auto_bookmark`: If the user selects a bookmark from the address bar's
autocomplete suggestions (or perhaps a history item that was originally a
bookmark), the `transitionType` could be `auto_bookmark`, while the
`transitionQualifiers` would include `from_address_bar` because the action
started there.
* `reload`: If the user reloads the page by pressing Enter in the address bar
(after the URL is already there), the `transitionType` would be `reload`, and
`transitionQualifiers` would include `from_address_bar`. This also applies to
session restore or reopening closed tabs, where the navigation is treated as
a reload originating from the address bar's context.
---
In summary:
* `typed` is the most direct consequence of using the address bar to navigate to a specific URL.
* `generated` covers using the address bar for searches.
* `auto_bookmark` covers using the address bar to select a bookmark.
* `reload` covers re-submitting the current URL from the address bar.
# Problems
[Browser Restart]: #browser-restart
## Browser Restart
On browser restart, all existing windows and tabs need to be re-associated with
their Rewin history records.
Here's a [Stackoverflow question](https://stackoverflow.com/a/14518800/351162)
discussing how to do this in Chrome (but it is applicable to Firefox as well):
> Tabs can be (almost) uniquely and consistently identified as
> required by the question by maintaining a register of tabs which stores the
> following combination of variables in local persistent storage:
>
> - `Tab.id`
> - `Tab.index`
> - A 'fingerprint' of the document open in the tab - `[location.href, document.referrer, history.length]`
>
> These variables can be tracked and stored in the registry using listeners on
> a combination of the following events: `onUpdated` / `onCreated` / `onMoved`
> / `onDetached` / `onAttached` / `onRemoved` / `onReplaced`
>
> There are still ways to fool this method, but in practice they are
> probably pretty rare - mostly edge cases.
>
> Since it looks like I'm not the only one who has needed to solve this
> problem, I built my implementation as a library with the intention that it
> could be used in any Chrome extension. It's MIT licensed and [available on
> GitHub](https://github.com/drzax/chrome-tab-registry) for forking and pull
> requests (in fact, any feedback would be welcome - there are definitely
> possible improvements).
## Detecting History Navigation
In order to maintain it's internal history records, Rewin needs to be able to
distinguish when the user navigates by following a link, from when history is
used to navigate.
The [webNavigation API] can be used to figure out what kind of user interaction
was used to navigate to a new URL.
With `webNavigation.TransitionQualifier` `'forward_back'`
[Browser History Menu]: #browser-history-menu
## Browser History Menu
Functions used to populate a tab's browser history *only sometimes* affect the
history menus of the "Back" (←) and "Forward" (→) buttons in the browser.
However, this is not the programmatic view---from inside the extension,
updating the history works just fine---it is just the state is not shown in the
browser menus.
What's even more frustrating is that the above is *not always the case.* I've
tried to figure out what causes the weird behavior, thinking that it might be
tied to Firefox's [features gated by user activation], or the limitation for
[when popstate is sent] that suppresses popstate events if the user haven't yet
interacted with the tab in question, however, this does not seem to be the
case.---Or, if it is, then that is only part of the story.
### When it Works
The browser `history` state seems to always be consistent from inside
Javascript. Loading a new page, or using `pushState()` always increases
`history.length` by one, regardless of whether an entry is added to the Back
button menu or not, and the navigation methods (`back()`, `forward()` and
`go()`) always take you to the place you expect, regardless of whether that
history entry is visible in the browser menu or not.
I *think* the above is true regardless of what method is used to navigate,
though I have mostly experimented using `pushState()`---but it seems to me that
navigation based on `location` or `browser.tabs` have demonstrated the same
behavior.
In the instances I have been able to update the Back button menus, this has
always been triggered by user interaction. For example, displaying a button on
the web page itself, and have that call a function that does repeated
`pushState()` *sometimes* work to update browser Back button menu, but
sometimes not. When updating the history state from an extension, or directly
on page load, however, I have never been able to get the browser menu to
change.—It's also interesting to note that this does not seem related to the
speed with which the updates are done, using `setTimeout()` to space out the
`pushState()` invocations does not seem to affect the result one way or the
other.
Actually, that's not *entirely* true. When using `setTimeout()` and adding
history events with a long delay, the *current page* is always included in the
menu, and if the user opens the menu that current page is retained in the menu,
even after the text page is loaded. Lets say I have a Javascript that uses
`pushState()` to update the history menu with a 10 second interval. First I
load page A, this page will then be visible in the Back button menu until page
B is loaded, whereupon page B is visible in the menu until page C is loaded
(and so on). *However,* if the user happen to open the menu while (say) page B
was loaded (and then closed the menu again, without clicking any of the entries
therein) then the menu entry for page B will be retained, even after page C, D
and E have been loaded. Thus, the current page, plus any page that was loaded
when the user opened the menu will be retained in the menu.
### Restoring History
Currently, the tab history is populated with "fake" history events, which is
then loaded for real (by a content script) when a user navigate to that history
entry. The reason for this is that `history.pushState()` is used to populate
the history, and it can only be used for URLs with the same [origin] as the
current page (that is, URLs that have the same *protocol* and *domain*).
This means that history entries that have been restored by Rewin does not
contain "true" content, but a state that refers to the actual URL, and only
when the user navigates to one of these history entries, is the true content
loaded (by means of `location.replace()`).
Different ways of loading adding an entry to history:
* [History API]---Use `pushState()` create fake history entries, then use
content script to load the real page when user navigates to the entry.
* [Tabs API] (for browser extensions)---Use `create()` and `update()`
methods are able to load arbitrary addresses.
* [Window location property]---Use its `assign()` or `replace()` methods.
* Link injection (insert a link into the DOM, then click it).
* Form injection (same as above, but use a form instead).
* [Proxy API]---I don't know if this is suitable. Also, it's incompatible with
the corresponding Chrome API, so maybe avoid on the grounds that it'll make
it harder to make a Chrome extension in the future?
# Afterword
There are so many ways one can go with this, and I'm trying not to let my
fantastical wishes run too wild.
Sometime in the future I'd like to be able to "see my own trail" across the
internet, so that if I ever revisit a web page there'll be some visual
indicator in the browser showing that I've been there before.---From there I'd
like to be able to explore the context of my previous visit, see where I came
from last I was there, see what other tabs where open at that time, and be able
to explore my past history.
Maybe even be able to leave some notes to my future self? Or at least leave
myself a thumbs up/thumbs down---indicating to my future self whether or not I
found the page useful. (I imagine a thumbs up/thumbs down emoji would replace
the current bookmark star, in the Firefox URL bar---and in practice, I'm
thinking this would just be two bookmark folders, likewise named thumbs
up/thumbs down.)
# Related Research
* [Bugzilla #1378651: Allow accessing back-forward history for each tab]
* [Bugzilla #1381922: Allow modifying/restoring back-forward history for each tab]
* [Bugzilla #1413525: Implement a general mechanism for saving sessions]
* [Chromium Bug #40384007: History of a specific TAB]
* [Tab-Session-Manager Github Issue: Preserve Tab History #27]
* [Reddit r/firefox: Session manager addon that restores history of tabs as well]
* [Reddit r/firefox: Are there really no session manager add-ons that can save tab history?]
## Alternative Tab Interfaces
* [Lossless Web Navigation with Trails]---Argues that when link is opened in a
new tab (or window) it should to retain all its history (as if 'Duplicate
Tab' was clicked first). Each tab thus would have its own "trail" through the
history, some of which would be shared with other tabs.
* [A Spacial Model for Lossless Web Navigation]---Suggests a user interface for
navigating back and forward in history, but also "vertically" between trails.
-------------------------------------------------------------------------------
[A Spacial Model for Lossless Web Navigation]: https://www.freecodecamp.org/news/lossless-web-navigation-spatial-model-37f83438201d
[Bugzilla #1378651: Allow accessing back-forward history for each tab]: https://bugzilla.mozilla.org/show_bug.cgi?id=1378651
[Bugzilla #1381922: Allow modifying/restoring back-forward history for each tab]: https://bugzilla.mozilla.org/show_bug.cgi?id=1381922
[Bugzilla #1413525: Implement a general mechanism for saving sessions]: https://bugzilla.mozilla.org/show_bug.cgi?id=1413525
[Chromium Bug #40384007: History of a specific TAB]: https://issues.chromium.org/issues/40384007
[Features gated by user activation]: https://developer.mozilla.org/en-US/docs/Web/Security/User_activation "MDN: Features gated by user activation"
[History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API "MDN: Working with the History API"
[Lossless Web Navigation with Trails]: https://www.freecodecamp.org/news/lossless-web-navigation-with-trails-9cd48c0abb56
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "MDN: Glossary Origin"
[Proxy API]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy "Proxy API"
[Reddit r/firefox: Are there really no session manager add-ons that can save tab history?]: https://reddit.com/r/firefox/comments/grs6a8/are_there_really_no_session_manager_addons_that/
[Reddit r/firefox: Session manager addon that restores history of tabs as well]: https://www.reddit.com/r/firefox/comments/u3lyef/session_manager_addon_that_restores_history_of/
[Tab-Session-Manager Github Issue: Preserve Tab History #27]: https://github.com/sienori/Tab-Session-Manager/issues/27
[Tabs API]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs "MDN: Javascripts APIs: Tabs"
[When popstate is sent]: https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent "MDN: Window: Popstate Event: When Popstate Is Sent"
[webNavigation API]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation "MDN: webNavigation"
[Window location property]: https://developer.mozilla.org/en-US/docs/Web/API/Window/location "MDN: Window: location property"