Commit 48c7cb60 authored by Narcis Beleuzu's avatar Narcis Beleuzu
Browse files

Backed out changeset fa117a5fa4cc (bug 1816137) for bc failure on...

Backed out changeset fa117a5fa4cc (bug 1816137) for bc failure on browser_all_files_referenced.js . CLOSED TREE
parent 5734c037
Loading
Loading
Loading
Loading
+0 −217
Original line number Diff line number Diff line
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
  ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
});

function isRedirectType(visitType) {
  const { TRANSITIONS } = lazy.PlacesUtils.history;
  return (
    visitType === TRANSITIONS.REDIRECT_PERMANENT ||
    visitType === TRANSITIONS.REDIRECT_TEMPORARY
  );
}

const BULK_PLACES_EVENTS_THRESHOLD = 50;

/**
 * An object that contains details of a page visit.
 *
 * @typedef {object} HistoryVisit
 *
 * @property {Date} date
 *   When this page was visited.
 * @property {number} id
 *   Visit ID from the database.
 * @property {string} title
 *   The page's title.
 * @property {string} url
 *   The page's URL.
 */

/**
 * Queries the places database using an async read only connection. Maintains
 * an internal cache of query results which is live-updated by adding listeners
 * to `PlacesObservers`. When the results are no longer needed, call `close` to
 * remove the listeners.
 */
export class PlacesQuery {
  /** @type HistoryVisit[] */
  #cachedHistory = null;
  /** @type object */
  #cachedHistoryOptions = null;
  /** @type function(PlacesEvent[]) */
  #historyListener = null;
  /** @type function(HistoryVisit[]) */
  #historyListenerCallback = null;

  /**
   * Get a snapshot of history visits at this moment.
   *
   * @param {object} [options]
   *   Options to apply to the database query.
   * @param {number} [options.daysOld]
   *   The maximum number of days to go back in history.
   * @returns {HistoryVisit[]}
   *   History visits obtained from the database query.
   */
  async getHistory({ daysOld = 60 } = {}) {
    const options = { daysOld };
    const cacheInvalid =
      this.#cachedHistory == null ||
      !lazy.ObjectUtils.deepEqual(options, this.#cachedHistoryOptions);
    if (cacheInvalid) {
      this.#cachedHistory = [];
      this.#cachedHistoryOptions = options;
      const db = await lazy.PlacesUtils.promiseDBConnection();
      const sql = `SELECT v.id, visit_date, title, url, visit_type, from_visit, hidden
        FROM moz_historyvisits v
        JOIN moz_places h
        ON v.place_id = h.id
        WHERE visit_date >= (strftime('%s','now','localtime','start of day','-${Number(
          daysOld
        )} days','utc') * 1000000)
        ORDER BY visit_date DESC`;
      const rows = await db.executeCached(sql);
      let lastUrl; // Avoid listing consecutive visits to the same URL.
      let lastRedirectFromVisitId; // Avoid listing redirecting visits.
      for (const row of rows) {
        const [
          id,
          visitDate,
          title,
          url,
          visitType,
          fromVisit,
          hidden,
        ] = Array.from({ length: row.numEntries }, (_, i) =>
          row.getResultByIndex(i)
        );
        if (isRedirectType(visitType) && fromVisit > 0) {
          lastRedirectFromVisitId = fromVisit;
        }
        if (!hidden && url !== lastUrl && id !== lastRedirectFromVisitId) {
          this.#cachedHistory.push({
            date: lazy.PlacesUtils.toDate(visitDate),
            id,
            title,
            url,
          });
          lastUrl = url;
        }
      }
    }
    if (!this.#historyListener) {
      this.#initHistoryListener();
    }
    return this.#cachedHistory;
  }

  /**
   * Observe changes to the visits table. When changes are made, the callback
   * is given the new list of visits. Only one callback can be active at a time
   * (per instance). If one already exists, it will be replaced.
   *
   * @param {function(HistoryVisit[])} callback
   *   The function to call when changes are made.
   */
  observeHistory(callback) {
    this.#historyListenerCallback = callback;
  }

  /**
   * Close this query. Caches are cleared and listeners are removed.
   */
  close() {
    this.#cachedHistory = null;
    this.#cachedHistoryOptions = null;
    PlacesObservers.removeListener(
      ["page-removed", "page-visited", "history-cleared", "page-title-changed"],
      this.#historyListener
    );
    this.#historyListener = null;
    this.#historyListenerCallback = null;
  }

  /**
   * Listen for changes to the visits table and update caches accordingly.
   */
  #initHistoryListener() {
    this.#historyListener = async events => {
      if (
        events.length >= BULK_PLACES_EVENTS_THRESHOLD ||
        events.some(({ type }) => type === "page-removed")
      ) {
        // Accounting for cascading deletes, or handling places events in bulk,
        // can be expensive. In this case, we invalidate the cache once rather
        // than handling each event individually.
        this.#cachedHistory = null;
      } else if (this.#cachedHistory != null) {
        for (const event of events) {
          switch (event.type) {
            case "page-visited":
              await this.#handlePageVisited(event);
              break;
            case "history-cleared":
              this.#cachedHistory = [];
              break;
            case "page-title-changed":
              this.#cachedHistory
                .filter(({ url }) => url === event.url)
                .forEach(visit => (visit.title = event.title));
              break;
          }
        }
      }
      if (typeof this.#historyListenerCallback === "function") {
        lazy.requestIdleCallback(async () => {
          const history = await this.getHistory(this.#cachedHistoryOptions);
          this.#historyListenerCallback(history);
        });
      }
    };
    PlacesObservers.addListener(
      ["page-removed", "page-visited", "history-cleared", "page-title-changed"],
      this.#historyListener
    );
  }

  /**
   * Handle a page visited event.
   *
   * @param {PlacesEvent} event
   *   The event.
   */
  async #handlePageVisited(event) {
    const lastVisit = this.#cachedHistory[0];
    if (
      lastVisit != null &&
      (event.url === lastVisit.url ||
        (isRedirectType(event.transitionType) &&
          event.referringVisitId === lastVisit.id))
    ) {
      // Remove the last visit if it duplicates this visit's URL, or if it
      // redirects to this visit.
      this.#cachedHistory.shift();
    }
    if (!event.hidden) {
      this.#cachedHistory.unshift({
        date: new Date(event.visitTime),
        id: event.visitId,
        title: event.lastKnownTitle,
        url: event.url,
      });
    }
  }
}
+0 −1
Original line number Diff line number Diff line
@@ -68,7 +68,6 @@ if CONFIG["MOZ_PLACES"]:
        "PlacesExpiration.sys.mjs",
        "PlacesFrecencyRecalculator.sys.mjs",
        "PlacesPreviews.sys.mjs",
        "PlacesQuery.sys.mjs",
        "PlacesSyncUtils.sys.mjs",
        "PlacesTransactions.sys.mjs",
        "PlacesUtils.sys.mjs",
+0 −100
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { PlacesQuery } = ChromeUtils.importESModule(
  "resource://gre/modules/PlacesQuery.sys.mjs"
);

add_task(async function test_visits_cache_is_updated() {
  const placesQuery = new PlacesQuery();
  const now = new Date();
  info("Insert the first visit.");
  await PlacesUtils.history.insert({
    url: "https://www.example.com/",
    title: "Example Domain",
    visits: [{ date: now }],
  });
  let history = await placesQuery.getHistory();
  Assert.equal(history.length, 1);
  Assert.equal(history[0].url, "https://www.example.com/");
  Assert.equal(history[0].date.getTime(), now.getTime());
  Assert.equal(history[0].title, "Example Domain");

  info("Insert the next visit.");
  let historyUpdated = PromiseUtils.defer();
  placesQuery.observeHistory(newHistory => {
    history = newHistory;
    historyUpdated.resolve();
  });
  await PlacesUtils.history.insert({
    url: "https://example.net/",
    visits: [{ date: now }],
  });
  await historyUpdated.promise;
  Assert.equal(history.length, 2);
  Assert.equal(
    history[0].url,
    "https://example.net/",
    "The most recent visit should come first."
  );
  Assert.equal(history[0].date.getTime(), now.getTime());

  info("Remove the first visit.");
  historyUpdated = PromiseUtils.defer();
  await PlacesUtils.history.remove("https://www.example.com/");
  await historyUpdated.promise;
  Assert.equal(history.length, 1);
  Assert.equal(history[0].url, "https://example.net/");

  info("Remove all visits.");
  historyUpdated = PromiseUtils.defer();
  await PlacesUtils.history.clear();
  await historyUpdated.promise;
  Assert.equal(history.length, 0);
  placesQuery.close();
});

add_task(async function test_filter_visits_by_age() {
  const placesQuery = new PlacesQuery();
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      visits: [{ date: new Date("2000-01-01T12:00:00") }],
    },
    {
      url: "https://example.net/",
      visits: [{ date: new Date() }],
    },
  ]);
  const history = await placesQuery.getHistory({ daysOld: 1 });
  Assert.equal(history.length, 1, "The older visit should be excluded.");
  Assert.equal(history[0].url, "https://example.net/");
  await PlacesUtils.history.clear();
  placesQuery.close();
});

add_task(async function test_filter_redirecting_visits() {
  const placesQuery = new PlacesQuery();
  await PlacesUtils.history.insertMany([
    {
      url: "http://google.com/",
      visits: [{ transition: PlacesUtils.history.TRANSITIONS.TYPED }],
    },
    {
      url: "https://www.google.com/",
      visits: [
        {
          transition: PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT,
          referrer: "http://google.com/",
        },
      ],
    },
  ]);
  const history = await placesQuery.getHistory();
  Assert.equal(history.length, 1, "Redirecting visits should be excluded.");
  Assert.equal(history[0].url, "https://www.google.com/");
  await PlacesUtils.history.clear();
  placesQuery.close();
});
+0 −1
Original line number Diff line number Diff line
@@ -39,7 +39,6 @@ skip-if = os == "linux" # Bug 821781
[test_1105208.js]
[test_1105866.js]
[test_1606731.js]
[test_PlacesQuery_history.js]
[test_asyncExecuteLegacyQueries.js]
[test_async_transactions.js]
[test_autocomplete_match_fallbackTitle.js]