Source code

Revision control

Copy as Markdown

Other Tools

/* 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";
const {
UPDATE_SESSION_HISTORY,
UPDATE_SESSION_HISTORY_ENTRY,
} = require("resource://devtools/client/application/src/constants.js");
function SessionHistory() {
return {
count: 0,
current: 0,
rows: [],
entriesByKey: {},
};
}
function sessionHistoryReducer(state = SessionHistory(), action) {
switch (action.type) {
case UPDATE_SESSION_HISTORY: {
const { sessionHistory } = action;
const entriesByKey = {};
return Object.assign({}, state, {
count: sessionHistory.count,
current: sessionHistory.index,
rows: createRows(sessionHistory, entriesByKey),
entriesByKey,
});
}
case UPDATE_SESSION_HISTORY_ENTRY: {
const { sessionHistoryEntry } = action;
const entryKey = key(sessionHistoryEntry.ID);
const entry = state.entriesByKey[entryKey];
if (!entry) {
return state;
}
return {
...state,
entriesByKey: {
...state.entriesByKey,
[entryKey]: {
...entry,
// only title can be updated at the moment
title: sessionHistoryEntry.title,
},
},
};
}
default:
return state;
}
}
/**
* 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.toString();
}
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 createRows(sessionHistory, entriesByKey) {
const lookup = new Map();
const size = sessionHistory.count;
// 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 = 0; index < size; index++) {
const root = sessionHistory.getEntryAtIndex(index);
rootDiagram.addChild(root, index);
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);
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);
}
let previous = Diagram.EMPTY;
let newEntry = null;
while (entries.length) {
const entry = entries.shift();
if (entry !== Diagram.EMPTY && previous.ID != entry.ID) {
const url = new 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 = 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;
}
entriesByKey[key(entry.ID)] = {
url,
title: entry.title,
name: entry.name,
id: entry.navigationId,
key: entry.navigationKey,
bfcache: entry.isInBFCache,
...extra,
};
row.push(newEntry);
} else if (entry !== Diagram.EMPTY || previous.empty == Diagram.EMPTY) {
newEntry.age++;
} else {
newEntry = { age: 1 };
row.push(newEntry);
}
previous = entry;
}
return row;
}
).filter(row => row.length);
}
module.exports = {
SessionHistory,
sessionHistoryReducer,
};