software/hoardy-web/./extension/page/state.js

Passively capture, archive, and hoard your web browsing history, including the contents of the pages you visit, for later offline viewing, replay, mirroring, data scraping, and/or indexing. Your own personal private Wayback Machine that can also archive HTTP POST requests and responses, as well as most other HTTP-level data.

Files

Raw Source

Contents

/*
 * Copyright (c) 2023-2026 Jan Malakhovski <oxij@oxij.org>
 *
 * This file is a part of `hoardy-web` project.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * The "Internal State" page.
 */

"use strict";

narrowSessionId = getMapURLParam(statePageURL, "session", document.location, toNumber, null, null);
narrowWindowId = getMapURLParam(statePageURL, "window", document.location, toNumber, null, null);
narrowTabId =
    narrowWindowId === null
        ? getMapURLParam(statePageURL, "tab", document.location, toNumber, null, null)
        : null;

let defRRFilter = { sessionId: narrowSessionId, windowId: narrowWindowId, tabId: narrowTabId };
let rrfilters = {
    inFlight: mkReqresFilter(defRRFilter),
    problematic: mkReqresFilter(defRRFilter),
    inLimbo: mkReqresFilter(defRRFilter),
    log: mkReqresFilter(defRRFilter),
    queued: mkReqresFilter(defRRFilter),
    unarchived: mkReqresFilter(defRRFilter),
    buggedOut: mkReqresFilter(defRRFilter),
};
let dataNodeUpdaters = mkDataNodeUpdaters(rrfilters);

async function stateMain() {
    setPageLoading();

    await commonMain();

    let config;

    let titleParts = [];
    if (narrowWindowId !== null) {
        titleParts.push(`Window #${narrowWindowId}`);
    }
    if (narrowTabId !== null) {
        titleParts.push(`Tab #${narrowTabId}`);
    }
    if (narrowSessionId !== null && thisSessionId !== narrowSessionId) {
        titleParts.push(`of Session #${narrowSessionId}`);
    }
    document.title += ": " + titleParts.join(" ");

    buttonToMessage("stopStarInFlight", () => ["stopInFlight", rrfilters.inFlight]);
    buttonToMessage("forgetStarLog", () => ["forgetLog", rrfilters.log]);
    buttonToMessage("rotate1Problematic", () => [
        "rotateProblematic",
        assignRec({}, rrfilters.problematic, { limit: 1 }),
    ]);
    buttonToMessage("unmark1Problematic", () => [
        "unmarkProblematic",
        assignRec({}, rrfilters.problematic, { limit: 1 }),
    ]);
    buttonToMessage("unmarkStarProblematic", () => ["unmarkProblematic", rrfilters.problematic]);
    buttonToMessage("rotate1InLimbo", () => [
        "rotateInLimbo",
        assignRec({}, rrfilters.inLimbo, { limit: 1 }),
    ]);
    buttonToMessage("discard1InLimbo", () => [
        "popInLimbo",
        false,
        assignRec({}, rrfilters.inLimbo, { limit: 1 }),
    ]);
    buttonToMessage("discardStarInLimbo", () => ["popInLimbo", false, rrfilters.inLimbo]);
    buttonToMessage("collect1InLimbo", () => [
        "popInLimbo",
        true,
        assignRec({}, rrfilters.inLimbo, { limit: 1 }),
    ]);
    buttonToMessage("collectStarInLimbo", () => ["popInLimbo", true, rrfilters.inLimbo]);
    buttonToMessage("retryStarUnarchived", () => ["retryUnarchived", true, rrfilters.unarchived]);
    buttonToMessage("archiveStarBuggedOut", () => ["archiveBuggedOut", rrfilters.buggedOut]);
    buttonToMessage("deleteStarBuggedOut", () => ["deleteBuggedOut", rrfilters.buggedOut]);

    for (let id of Object.keys(rrfilters)) {
        let cid = capitalize(id);
        let getCid = ["get" + cid];
        let resetCid = "reset" + cid;
        let resetFunc = dataNodeUpdaters[resetCid];

        if (resetFunc === undefined) {
            console.error("no node updater for", id);
            continue;
        }

        let reset = setUI(
            document,
            "rrfilters." + id,
            rrfilters[id],
            (_value, _path, resetting) => {
                resetSingletonTimeout(scheduledUI, resetCid, resetting ? 300 : 0, () =>
                    browser.runtime.sendMessage(getCid).then(resetFunc).catch(logError),
                );
            },
        );
        buttonToAction("reset-rrfilters." + id, reset);
    }

    async function updateConfig(nconfig) {
        if (nconfig === undefined) {
            config = await browser.runtime.sendMessage(["getConfig"]);
        } else {
            config = nconfig;
        }

        setRootClasses(config);
    }

    async function processUpdate(update) {
        let [what, data] = update;
        switch (what) {
            case "updateConfig":
                await updateConfig(data);
                return;
            default: {
                let updateFunc = dataNodeUpdaters[what];
                if (updateFunc !== undefined) {
                    return updateFunc(data);
                }

                let res = await webextRPCHandleMessageDefault(update);
                return res;
            }
        }
    }

    setPageSettling();

    await subscribeToExtension(
        "state" + (narrowTabId !== null ? `#${narrowTabId}` : ""),
        3,
        async (isInvalid) => {
            await updateConfig();
            let res = {};
            for (let id of Object.keys(rrfilters)) {
                res[id] = await browser.runtime.sendMessage(["get" + capitalize(id)]);
                if (isInvalid()) {
                    return;
                }
            }

            for (let id of Object.keys(rrfilters)) {
                dataNodeUpdaters["reset" + capitalize(id)](res[id]);
            }
        },
        asyncNoop,
        processUpdate,
    );

    if (config.debugRuntime) {
        setTimeout(() => verifyLinks(document, console.error, true), 100);
    }

    setPageDone();

    // force re-scroll
    viewHashNode((id) => (id === "tail" ? scrollEndIntoView : undefined));
}

document.addEventListener("DOMContentLoaded", () => stateMain().catch(setPageError), setPageError);