We were only using BrowsingContext (derivated) attributes. Also BrowsingContext is more convenient to use as it doesn't change when moving the tab to another window. Differential Revision: https://phabricator.services.mozilla.com/D307200
337 lines
9.2 KiB
JavaScript
337 lines
9.2 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
class SessionHistoryWatcher {
|
|
#entriesByKey = {};
|
|
#sessionHistory;
|
|
#sessionHistoryListener;
|
|
#currentEntry = null;
|
|
|
|
/**
|
|
* Start watching (from the parent process) for session history updates related to a
|
|
* given Watcher Actor.
|
|
*
|
|
* @param {WatcherActor} watcherActor: The watcher actor from which we should observe
|
|
* document event
|
|
* @param {object} options
|
|
* @param {Function} options.onAvailable
|
|
* @param {Function} options.onUpdated
|
|
* @param {Function} options.onDestroyed
|
|
*/
|
|
async watch(watcherActor, { onAvailable, onUpdated, onDestroyed }) {
|
|
this.watcherActor = watcherActor;
|
|
this.onAvailable = onAvailable;
|
|
this.onUpdated = onUpdated;
|
|
this.onDestroyed = onDestroyed;
|
|
|
|
this.#sessionHistory = BrowsingContext.get(
|
|
this.watcherActor.browsingContext.id
|
|
).sessionHistory;
|
|
|
|
this.#processSessionHistory();
|
|
this.#currentEntry = this.#sessionHistory.getEntryAtIndex(
|
|
this.#sessionHistory.index
|
|
);
|
|
|
|
this.#sessionHistoryListener = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsISHistoryListener",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
OnHistoryCommit: () => {
|
|
const newEntry = this.#sessionHistory.getEntryAtIndex(
|
|
this.#sessionHistory.index
|
|
);
|
|
const isUpdate = this.#currentEntry?.sharesDocumentWith(newEntry);
|
|
this.#currentEntry = newEntry;
|
|
this.#processSessionHistory(isUpdate);
|
|
},
|
|
OnEntryUpdated: entry => {
|
|
const title = entry.title;
|
|
const url = entry.URI.spec;
|
|
const name = entry.name;
|
|
this.onUpdated([
|
|
{
|
|
resourceId: this.resourceId,
|
|
resourceUpdates: {
|
|
sessionHistoryEntry: { url, title, name, key: key(entry.ID) },
|
|
},
|
|
browsingContextID: this.watcherActor.browsingContext.id,
|
|
},
|
|
]);
|
|
},
|
|
};
|
|
|
|
this.#sessionHistory.addSHistoryListener(this.#sessionHistoryListener);
|
|
}
|
|
|
|
get resourceId() {
|
|
return "session-history-" + this.watcherActor.browsingContext.browserId;
|
|
}
|
|
|
|
/**
|
|
* Will go over the session history and call onAvailable or onUpdated.
|
|
*
|
|
* @param {boolean} isUpdate When true, delivers changes via onUpdated instead of onAvailable.
|
|
*/
|
|
#processSessionHistory(isUpdate = false) {
|
|
const sessionHistorydiagrams = [];
|
|
for (const { start, end } of sameDocuments(this.#sessionHistory)) {
|
|
const rows = createRows(
|
|
this.#sessionHistory,
|
|
this.#entriesByKey,
|
|
start,
|
|
end
|
|
);
|
|
sessionHistorydiagrams.push({ rows, start, end });
|
|
}
|
|
|
|
const resource = {
|
|
resourceId: this.resourceId,
|
|
browsingContextID: this.watcherActor.browsingContext.id,
|
|
count: this.#sessionHistory.count,
|
|
current: this.#sessionHistory.index,
|
|
diagrams: sessionHistorydiagrams,
|
|
entriesByKey: this.#entriesByKey,
|
|
};
|
|
|
|
if (isUpdate) {
|
|
this.onUpdated([
|
|
{
|
|
resourceId: this.resourceId,
|
|
resourceUpdates: { sessionHistory: resource },
|
|
browsingContextID: this.watcherActor.browsingContext.id,
|
|
},
|
|
]);
|
|
} else {
|
|
this.onAvailable([resource]);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.#sessionHistory.removeSHistoryListener(this.#sessionHistoryListener);
|
|
this.#sessionHistory = null;
|
|
this.#sessionHistoryListener = null;
|
|
this.#currentEntry = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a tree-like data structure, performs a pre-order (also known as tree
|
|
* order) traversal of that tree. Since the elements can be anything, the
|
|
* caller needs to supply a way to find how many children a specific node has
|
|
* as well as a way to iterate children.
|
|
*
|
|
* @param {*} root Starting node of the traversal
|
|
* @param {function(*): int} getChildCount Helper to find child count
|
|
* @param {function(*, int): *} getChildAtIndex Helper to find child at index
|
|
*/
|
|
function* preOrderTraversal(root, getChildCount, getChildAtIndex) {
|
|
const queue = [root];
|
|
while (queue.length) {
|
|
const entry = queue.shift();
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
|
|
for (let index = getChildCount(entry) - 1; index >= 0; --index) {
|
|
queue.unshift(getChildAtIndex(entry, index));
|
|
}
|
|
|
|
yield entry;
|
|
}
|
|
}
|
|
|
|
function key(k) {
|
|
return `${k}`;
|
|
}
|
|
|
|
class Diagram {
|
|
#parent;
|
|
#entry;
|
|
#key;
|
|
#rows = [];
|
|
#lookup = new Map();
|
|
|
|
static EMPTY = Symbol("Diagram.EMPTY");
|
|
|
|
/**
|
|
* Create a session history diagram from a particular entry.
|
|
*
|
|
* @param {object|null} parent Parent node
|
|
* @param {object} entry Entry to create the diagram from
|
|
*/
|
|
constructor(parent, entry) {
|
|
this.#parent = parent;
|
|
this.#entry = entry;
|
|
this.#key = key(entry.docshellID);
|
|
}
|
|
|
|
get entry() {
|
|
return this.#entry;
|
|
}
|
|
|
|
get key() {
|
|
return this.#key;
|
|
}
|
|
|
|
get parent() {
|
|
return this.#parent;
|
|
}
|
|
|
|
get rows() {
|
|
return this.#rows;
|
|
}
|
|
|
|
lookup(id) {
|
|
return this.#lookup.get(id);
|
|
}
|
|
|
|
/**
|
|
* Append an entry to a row of the diagram.
|
|
*
|
|
* @param {object} entry New entry.
|
|
* @param {int} index Index to append at.
|
|
*/
|
|
addChild(entry, index) {
|
|
const id = key(entry.docshellID);
|
|
const row = this.#lookup.getOrInsertComputed(id, () => {
|
|
const newRow = new Array(index).fill(Diagram.EMPTY);
|
|
this.#rows.push({ id, newRow });
|
|
return newRow;
|
|
});
|
|
|
|
row.push(entry);
|
|
}
|
|
}
|
|
|
|
function* sameDocuments(sessionHistory) {
|
|
let previous = {
|
|
sharesDocumentWith() {
|
|
return true;
|
|
},
|
|
};
|
|
let start = 0;
|
|
const size = sessionHistory.count;
|
|
for (let index = 0; index < size; index++) {
|
|
const entry = sessionHistory.getEntryAtIndex(index);
|
|
const sameDocument = previous.sharesDocumentWith(entry);
|
|
previous = entry;
|
|
if (sameDocument) {
|
|
continue;
|
|
}
|
|
|
|
yield { start, end: index };
|
|
start = index;
|
|
}
|
|
|
|
yield { start, end: size };
|
|
}
|
|
|
|
function createRows(sessionHistory, entriesByKey, start, end) {
|
|
const lookup = new Map();
|
|
|
|
const size = end - start;
|
|
// We add a rootDiagram for convenience to be able to add top-level entries
|
|
// to it. This diagram will never be rendered to the full diagram, and the
|
|
// id will never be used in lookup.
|
|
const rootDiagram = new Diagram(null, { docshellID: "fakeDocShellID" });
|
|
const getChildCount = entry => entry.childCount;
|
|
const getChildAt = (entry, index) => entry.GetChildAt(index);
|
|
for (let index = start; index < end; index++) {
|
|
const root = sessionHistory.getEntryAtIndex(index);
|
|
rootDiagram.addChild(root, index - start);
|
|
lookup.getOrInsertComputed(key(root.docshellID), () => {
|
|
return new Diagram(rootDiagram, root);
|
|
});
|
|
|
|
for (const entry of preOrderTraversal(root, getChildCount, getChildAt)) {
|
|
const parent = entry.parent;
|
|
if (parent) {
|
|
const diagram = lookup.get(key(parent.docshellID));
|
|
diagram.addChild(entry, index - start);
|
|
lookup.getOrInsertComputed(key(entry.docshellID), () => {
|
|
return new Diagram(diagram, entry);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(
|
|
preOrderTraversal(
|
|
rootDiagram,
|
|
entry => entry.rows.length,
|
|
({ rows }, index) => lookup.get(rows[index].id)
|
|
),
|
|
diagram => {
|
|
if (!diagram.parent) {
|
|
return [];
|
|
}
|
|
|
|
const row = [];
|
|
const entries = diagram.parent.lookup(diagram.key);
|
|
for (let count = entries.length; count > size; count--) {
|
|
entries.push(Diagram.EMPTY);
|
|
}
|
|
while (entries.length < size) {
|
|
entries.push(Diagram.EMPTY);
|
|
}
|
|
let previous = Diagram.EMPTY;
|
|
let newEntry = null;
|
|
while (entries.length) {
|
|
const entry = entries.shift();
|
|
|
|
if (entry !== Diagram.EMPTY && previous.ID != entry.ID) {
|
|
const url = entry.URI.spec;
|
|
newEntry = {
|
|
age: 1,
|
|
key: key(entry.ID),
|
|
sameDocNav:
|
|
previous !== Diagram.EMPTY && entry.sharesDocumentWith(previous),
|
|
};
|
|
const parent = entry.parent?.navigationId;
|
|
const extra = {};
|
|
if (parent) {
|
|
extra.parent = key(parent);
|
|
}
|
|
const numChildren = getChildCount(entry);
|
|
if (numChildren) {
|
|
const children = new Array(numChildren);
|
|
for (let index = 0; index < numChildren; ++index) {
|
|
children[index] = getChildAt(entry, index)?.navigationId;
|
|
}
|
|
extra.children = children.map(key);
|
|
}
|
|
|
|
entriesByKey[key(entry.ID)] = {
|
|
url,
|
|
title: entry.title,
|
|
name: entry.name,
|
|
id: key(entry.navigationId),
|
|
key: key(entry.navigationKey),
|
|
bfcache: entry.isInBFCache,
|
|
...extra,
|
|
};
|
|
row.push(newEntry);
|
|
} else if (!newEntry) {
|
|
newEntry = { age: 1 };
|
|
row.push(newEntry);
|
|
} else if (entry !== Diagram.EMPTY || previous === Diagram.EMPTY) {
|
|
newEntry.age++;
|
|
} else {
|
|
newEntry = { age: 1 };
|
|
row.push(newEntry);
|
|
}
|
|
previous = entry;
|
|
}
|
|
|
|
return row;
|
|
}
|
|
).filter(row => row.length);
|
|
}
|
|
|
|
module.exports = SessionHistoryWatcher;
|