gecko/toolkit/components/thumbnails/PageThumbs.jsm

817 lines
26 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";
this.EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
const LATEST_STORAGE_VERSION = 3;
const EXPIRATION_MIN_CHUNK_SIZE = 50;
const EXPIRATION_INTERVAL_SECS = 3600;
// If a request for a thumbnail comes in and we find one that is "stale"
// (or don't find one at all) we automatically queue a request to generate a
// new one.
const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
/**
* Name of the directory in the profile that contains the thumbnails.
*/
const THUMBNAIL_DIRECTORY = "thumbnails";
/**
* The default background color for page thumbnails.
*/
const THUMBNAIL_BG_COLOR = "#fff";
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gUpdateTimerManager",
"@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
});
XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = 'utf8';
return converter;
});
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
/**
* Utilities for dealing with promises and Task.jsm
*/
const TaskUtils = {
/**
* Add logging to a promise.
*
* @param {Promise} promise
* @return {Promise} A promise behaving as |promise|, but with additional
* logging in case of uncaught error.
*/
captureErrors: function captureErrors(promise) {
return promise.then(
null,
function onError(reason) {
Cu.reportError("Uncaught asynchronous error: " + reason + " at\n"
+ reason.stack + "\n");
throw reason;
}
);
},
/**
* Spawn a new Task from a generator.
*
* This function behaves as |Task.spawn|, with the exception that it
* adds logging in case of uncaught error. For more information, see
* the documentation of |Task.jsm|.
*
* @param {generator} gen Some generator.
* @return {Promise} A promise built from |gen|, with the same semantics
* as |Task.spawn(gen)|.
*/
spawn: function spawn(gen) {
return this.captureErrors(Task.spawn(gen));
},
/**
* Read the bytes from a blob, asynchronously.
*
* @return {Promise}
* @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
* @reject {DOMError} In case of error, the underlying DOMError.
*/
readBlob: function readBlob(blob) {
let deferred = Promise.defer();
let reader = Cc["@mozilla.org/files/filereader;1"].createInstance(Ci.nsIDOMFileReader);
reader.onloadend = function onloadend() {
if (reader.readyState != Ci.nsIDOMFileReader.DONE) {
deferred.reject(reader.error);
} else {
deferred.resolve(reader.result);
}
};
reader.readAsArrayBuffer(blob);
return deferred.promise;
}
};
/**
* Singleton providing functionality for capturing web page thumbnails and for
* accessing them if already cached.
*/
this.PageThumbs = {
_initialized: false,
/**
* The calculated width and height of the thumbnails.
*/
_thumbnailWidth : 0,
_thumbnailHeight : 0,
/**
* The scheme to use for thumbnail urls.
*/
get scheme() "moz-page-thumb",
/**
* The static host to use for thumbnail urls.
*/
get staticHost() "thumbnail",
/**
* The thumbnails' image type.
*/
get contentType() "image/png",
init: function PageThumbs_init() {
if (!this._initialized) {
this._initialized = true;
PlacesUtils.history.addObserver(PageThumbsHistoryObserver, false);
// Migrate the underlying storage, if needed.
PageThumbsStorageMigrator.migrate();
PageThumbsExpiration.init();
}
},
uninit: function PageThumbs_uninit() {
if (this._initialized) {
this._initialized = false;
PlacesUtils.history.removeObserver(PageThumbsHistoryObserver);
}
},
/**
* Gets the thumbnail image's url for a given web page's url.
* @param aUrl The web page's url that is depicted in the thumbnail.
* @return The thumbnail image's url.
*/
getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
return this.scheme + "://" + this.staticHost +
"?url=" + encodeURIComponent(aUrl);
},
/**
* Gets the path of the thumbnail file for a given web page's
* url. This file may or may not exist depending on whether the
* thumbnail has been captured or not.
*
* @param aUrl The web page's url.
* @return The path of the thumbnail file.
*/
getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
return PageThumbsStorage.getFilePathForURL(aUrl);
},
/**
* Checks if an existing thumbnail for the specified URL is either missing
* or stale, and if so, queues a background request to capture it. That
* capture process will send a notification via the observer service on
* capture, so consumers should watch for such observations if they want to
* be notified of an updated thumbnail.
*
* @return {Promise} that's resolved on completion.
*/
captureIfStale: function PageThumbs_captureIfStale(aUrl) {
let deferredResult = Promise.defer();
let filePath = PageThumbsStorage.getFilePathForURL(aUrl);
PageThumbsWorker.post(
"isFileRecent",
[filePath, MAX_THUMBNAIL_AGE_SECS]
).then(result => {
if (!result.ok) {
// Sadly there is currently a circular dependency between this module
// and BackgroundPageThumbs, so do the import locally.
let BPT = Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}).BackgroundPageThumbs;
BPT.capture(aUrl, {onDone: deferredResult.resolve});
}
});
return deferredResult.promise;
},
/**
* Captures a thumbnail for the given window.
* @param aWindow The DOM window to capture a thumbnail from.
* @param aCallback The function to be called when the thumbnail has been
* captured. The first argument will be the data stream
* containing the image data.
*/
capture: function PageThumbs_capture(aWindow, aCallback) {
if (!this._prefEnabled()) {
return;
}
let canvas = this._createCanvas();
this.captureToCanvas(aWindow, canvas);
// Fetch the canvas data on the next event loop tick so that we allow
// some event processing in between drawing to the canvas and encoding
// its data. We want to block the UI as short as possible. See bug 744100.
Services.tm.currentThread.dispatch(function () {
canvas.mozFetchAsStream(aCallback, this.contentType);
}.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
},
/**
* Captures a thumbnail for the given window.
*
* @param aWindow The DOM window to capture a thumbnail from.
* @return {Promise}
* @resolve {Blob} The thumbnail, as a Blob.
*/
captureToBlob: function PageThumbs_captureToBlob(aWindow) {
if (!this._prefEnabled()) {
return null;
}
let canvas = this._createCanvas();
this.captureToCanvas(aWindow, canvas);
let deferred = Promise.defer();
let type = this.contentType;
// Fetch the canvas data on the next event loop tick so that we allow
// some event processing in between drawing to the canvas and encoding
// its data. We want to block the UI as short as possible. See bug 744100.
canvas.toBlob(function asBlob(blob) {
deferred.resolve(blob, type);
});
return deferred.promise;
},
/**
* Captures a thumbnail from a given window and draws it to the given canvas.
* @param aWindow The DOM window to capture a thumbnail from.
* @param aCanvas The canvas to draw to.
*/
captureToCanvas: function PageThumbs_captureToCanvas(aWindow, aCanvas) {
let telemetryCaptureTime = new Date();
this._captureToCanvas(aWindow, aCanvas);
let telemetry = Services.telemetry;
telemetry.getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
.add(new Date() - telemetryCaptureTime);
},
// The background thumbnail service captures to canvas but doesn't want to
// participate in this service's telemetry, which is why this method exists.
_captureToCanvas: function PageThumbs__captureToCanvas(aWindow, aCanvas) {
let [sw, sh, scale] = this._determineCropSize(aWindow, aCanvas);
let ctx = aCanvas.getContext("2d");
// Scale the canvas accordingly.
ctx.save();
ctx.scale(scale, scale);
try {
// Draw the window contents to the canvas.
ctx.drawWindow(aWindow, 0, 0, sw, sh, THUMBNAIL_BG_COLOR,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
} catch (e) {
// We couldn't draw to the canvas for some reason.
}
ctx.restore();
},
/**
* Captures a thumbnail for the given browser and stores it to the cache.
* @param aBrowser The browser to capture a thumbnail for.
* @param aCallback The function to be called when finished (optional).
*/
captureAndStore: function PageThumbs_captureAndStore(aBrowser, aCallback) {
if (!this._prefEnabled()) {
return;
}
let url = aBrowser.currentURI.spec;
let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec;
// see if this was an error response.
let wasError = this._isChannelErrorResponse(channel);
TaskUtils.spawn((function task() {
let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let buffer = yield TaskUtils.readBlob(blob);
yield this._store(originalURL, url, buffer, wasError);
} catch (_) {
isSuccess = false;
}
if (aCallback) {
aCallback(isSuccess);
}
}).bind(this));
},
/**
* Stores data to disk for the given URLs.
*
* NB: The background thumbnail service calls this, too.
*
* @param aOriginalURL The URL with which the capture was initiated.
* @param aFinalURL The URL to which aOriginalURL ultimately resolved.
* @param aData An ArrayBuffer containing the image data.
* @param aWasErrorResponse A boolean indicating if the capture was for a
* response that returned an error code.
* @return {Promise}
*/
_store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aWasErrorResponse) {
return TaskUtils.spawn(function () {
// If we got an error response, we only save it if we don't have an
// existing thumbnail. If we *do* have an existing thumbnail we "touch"
// it so we consider the old version fresh.
if (aWasErrorResponse) {
let result = yield PageThumbsStorage.touchIfExists(aFinalURL);
let exists = result.ok;
if (exists) {
if (aFinalURL != aOriginalURL) {
yield PageThumbsStorage.touchIfExists(aOriginalURL);
}
return;
}
// was an error response, but no existing thumbnail - just store
// that error response as something is (arguably) better than nothing.
}
let telemetryStoreTime = new Date();
yield PageThumbsStorage.writeData(aFinalURL, aData);
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
.add(new Date() - telemetryStoreTime);
Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
// We've been redirected. Create a copy of the current thumbnail for
// the redirect source. We need to do this because:
//
// 1) Users can drag any kind of links onto the newtab page. If those
// links redirect to a different URL then we want to be able to
// provide thumbnails for both of them.
//
// 2) The newtab page should actually display redirect targets, only.
// Because of bug 559175 this information can get lost when using
// Sync and therefore also redirect sources appear on the newtab
// page. We also want thumbnails for those.
if (aFinalURL != aOriginalURL) {
yield PageThumbsStorage.copy(aFinalURL, aOriginalURL);
Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
}
});
},
/**
* Register an expiration filter.
*
* When thumbnails are going to expire, each registered filter is asked for a
* list of thumbnails to keep.
*
* The filter (if it is a callable) or its filterForThumbnailExpiration method
* (if the filter is an object) is called with a single argument. The
* argument is a callback function. The filter must call the callback
* function and pass it an array of zero or more URLs. (It may do so
* asynchronously.) Thumbnails for those URLs will be except from expiration.
*
* @param aFilter callable, or object with filterForThumbnailExpiration method
*/
addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
PageThumbsExpiration.addFilter(aFilter);
},
/**
* Unregister an expiration filter.
* @param aFilter A filter that was previously passed to addExpirationFilter.
*/
removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
PageThumbsExpiration.removeFilter(aFilter);
},
/**
* Determines the crop size for a given content window.
* @param aWindow The content window.
* @param aCanvas The target canvas.
* @return An array containing width, height and scale.
*/
_determineCropSize: function PageThumbs_determineCropSize(aWindow, aCanvas) {
let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let sbWidth = {}, sbHeight = {};
try {
utils.getScrollbarSize(false, sbWidth, sbHeight);
} catch (e) {
// This might fail if the window does not have a presShell.
Cu.reportError("Unable to get scrollbar size in _determineCropSize.");
sbWidth.value = sbHeight.value = 0;
}
// Even in RTL mode, scrollbars are always on the right.
// So there's no need to determine a left offset.
let sw = aWindow.innerWidth - sbWidth.value;
let sh = aWindow.innerHeight - sbHeight.value;
let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas;
let scale = Math.min(Math.max(thumbnailWidth / sw, thumbnailHeight / sh), 1);
let scaledWidth = sw * scale;
let scaledHeight = sh * scale;
if (scaledHeight > thumbnailHeight)
sh -= Math.floor(Math.abs(scaledHeight - thumbnailHeight) * scale);
if (scaledWidth > thumbnailWidth)
sw -= Math.floor(Math.abs(scaledWidth - thumbnailWidth) * scale);
return [sw, sh, scale];
},
/**
* Creates a new hidden canvas element.
* @param aWindow The document of this window will be used to create the
* canvas. If not given, the hidden window will be used.
* @return The newly created canvas.
*/
_createCanvas: function PageThumbs_createCanvas(aWindow) {
let doc = (aWindow || Services.appShell.hiddenDOMWindow).document;
let canvas = doc.createElementNS(HTML_NAMESPACE, "canvas");
canvas.mozOpaque = true;
canvas.mozImageSmoothingEnabled = true;
let [thumbnailWidth, thumbnailHeight] = this._getThumbnailSize();
canvas.width = thumbnailWidth;
canvas.height = thumbnailHeight;
return canvas;
},
/**
* Calculates the thumbnail size based on current desktop's dimensions.
* @return The calculated thumbnail size or a default if unable to calculate.
*/
_getThumbnailSize: function PageThumbs_getThumbnailSize() {
if (!this._thumbnailWidth || !this._thumbnailHeight) {
let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
.getService(Ci.nsIScreenManager);
let left = {}, top = {}, width = {}, height = {};
screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
this._thumbnailWidth = Math.round(width.value / 3);
this._thumbnailHeight = Math.round(height.value / 3);
}
return [this._thumbnailWidth, this._thumbnailHeight];
},
/**
* Given a channel, returns true if it should be considered an "error
* response", false otherwise.
*/
_isChannelErrorResponse: function(channel) {
// No valid document channel sounds like an error to me!
if (!channel)
return true;
if (!(channel instanceof Ci.nsIHttpChannel))
// it might be FTP etc, so assume it's ok.
return false;
try {
return !channel.requestSucceeded;
} catch (_) {
// not being able to determine success is surely failure!
return true;
}
},
_prefEnabled: function PageThumbs_prefEnabled() {
try {
return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
}
catch (e) {
return true;
}
},
};
this.PageThumbsStorage = {
// The path for the storage
_path: null,
get path() {
if (!this._path) {
this._path = OS.Path.join(OS.Constants.Path.localProfileDir, THUMBNAIL_DIRECTORY);
}
return this._path;
},
ensurePath: function Storage_ensurePath() {
// Create the directory (ignore any error if the directory
// already exists). As all writes are done from the PageThumbsWorker
// thread, which serializes its operations, this ensures that
// future operations can proceed without having to check whether
// the directory exists.
return PageThumbsWorker.post("makeDir",
[this.path, {ignoreExisting: true}]).then(
null,
function onError(aReason) {
Components.utils.reportError("Could not create thumbnails directory" + aReason);
});
},
getLeafNameForURL: function Storage_getLeafNameForURL(aURL) {
if (typeof aURL != "string") {
throw new TypeError("Expecting a string");
}
let hash = this._calculateMD5Hash(aURL);
return hash + ".png";
},
getFilePathForURL: function Storage_getFilePathForURL(aURL) {
return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
},
/**
* Write the contents of a thumbnail, off the main thread.
*
* @param {string} aURL The url for which to store a thumbnail.
* @param {ArrayBuffer} aData The data to store in the thumbnail, as
* an ArrayBuffer. This array buffer is neutered and cannot be
* reused after the copy.
*
* @return {Promise}
*/
writeData: function Storage_writeData(aURL, aData) {
let path = this.getFilePathForURL(aURL);
this.ensurePath();
aData = new Uint8Array(aData);
let msg = [
path,
aData,
{
tmpPath: path + ".tmp",
bytes: aData.byteLength,
flush: false /*thumbnails do not require the level of guarantee provided by flush*/
}];
return PageThumbsWorker.post("writeAtomic", msg,
msg /*we don't want that message garbage-collected,
as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
memory tricks to enforce zero-copy*/);
},
/**
* Copy a thumbnail, off the main thread.
*
* @param {string} aSourceURL The url of the thumbnail to copy.
* @param {string} aTargetURL The url of the target thumbnail.
*
* @return {Promise}
*/
copy: function Storage_copy(aSourceURL, aTargetURL) {
this.ensurePath();
let sourceFile = this.getFilePathForURL(aSourceURL);
let targetFile = this.getFilePathForURL(aTargetURL);
return PageThumbsWorker.post("copy", [sourceFile, targetFile]);
},
/**
* Remove a single thumbnail, off the main thread.
*
* @return {Promise}
*/
remove: function Storage_remove(aURL) {
return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
},
/**
* Remove all thumbnails, off the main thread.
*
* @return {Promise}
*/
wipe: function Storage_wipe() {
return PageThumbsWorker.post("wipe", [this.path]);
},
/**
* If the file holding the thumbnail for the given URL exists, update the
* modification time of the file to now and return true, otherwise return
* false.
*
* @return {Promise}
*/
touchIfExists: function Storage_touchIfExists(aURL) {
return PageThumbsWorker.post("touchIfExists", [this.getFilePathForURL(aURL)]);
},
_calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
let hash = gCryptoHash;
let value = gUnicodeConverter.convertToByteArray(aValue);
hash.init(hash.MD5);
hash.update(value, value.length);
return this._convertToHexString(hash.finish(false));
},
_convertToHexString: function Storage_convertToHexString(aData) {
let hex = "";
for (let i = 0; i < aData.length; i++)
hex += ("0" + aData.charCodeAt(i).toString(16)).slice(-2);
return hex;
},
// Deprecated, please do not use
getFileForURL: function Storage_getFileForURL_DEPRECATED(aURL) {
Deprecated.warning("PageThumbs.getFileForURL is deprecated. Please use PageThumbs.getFilePathForURL and OS.File",
"https://developer.mozilla.org/docs/JavaScript_OS.File");
// Note: Once this method has been removed, we can get rid of the dependency towards FileUtils
return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
}
};
let PageThumbsStorageMigrator = {
get currentVersion() {
try {
return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
} catch (e) {
// The pref doesn't exist, yet. Return version 0.
return 0;
}
},
set currentVersion(aVersion) {
Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
},
migrate: function Migrator_migrate() {
let version = this.currentVersion;
// Storage version 1 never made it to beta.
// At the time of writing only Windows had (ProfD != ProfLD) and we
// needed to move thumbnails from the roaming profile to the locale
// one so that they're not needlessly included in backups and/or
// written via SMB.
// Storage version 2 also never made it to beta.
// The thumbnail folder structure has been changed and old thumbnails
// were not migrated. Instead, we just renamed the current folder to
// "<name>-old" and will remove it later.
if (version < 3) {
this.migrateToVersion3();
}
this.currentVersion = LATEST_STORAGE_VERSION;
},
/**
* Bug 239254 added support for having the disk cache and thumbnail
* directories on a local path (i.e. ~/.cache/) under Linux. We'll first
* try to move the old thumbnails to their new location. If that's not
* possible (because ProfD might be on a different file system than
* ProfLD) we'll just discard them.
*
* @param {string*} local The path to the local profile directory.
* Used for testing. Default argument is good for all non-testing uses.
* @param {string*} roaming The path to the roaming profile directory.
* Used for testing. Default argument is good for all non-testing uses.
*/
migrateToVersion3: function Migrator_migrateToVersion3(
local = OS.Constants.Path.localProfileDir,
roaming = OS.Constants.Path.profileDir) {
PageThumbsWorker.post(
"moveOrDeleteAllThumbnails",
[OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
OS.Path.join(local, THUMBNAIL_DIRECTORY)]
);
}
};
let PageThumbsExpiration = {
_filters: [],
init: function Expiration_init() {
gUpdateTimerManager.registerTimer("browser-cleanup-thumbnails", this,
EXPIRATION_INTERVAL_SECS);
},
addFilter: function Expiration_addFilter(aFilter) {
this._filters.push(aFilter);
},
removeFilter: function Expiration_removeFilter(aFilter) {
let index = this._filters.indexOf(aFilter);
if (index > -1)
this._filters.splice(index, 1);
},
notify: function Expiration_notify(aTimer) {
let urls = [];
let filtersToWaitFor = this._filters.length;
let expire = function expire() {
this.expireThumbnails(urls);
}.bind(this);
// No registered filters.
if (!filtersToWaitFor) {
expire();
return;
}
function filterCallback(aURLs) {
urls = urls.concat(aURLs);
if (--filtersToWaitFor == 0)
expire();
}
for (let filter of this._filters) {
if (typeof filter == "function")
filter(filterCallback)
else
filter.filterForThumbnailExpiration(filterCallback);
}
},
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
let path = this.path;
let keep = [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)];
let msg = [
PageThumbsStorage.path,
keep,
EXPIRATION_MIN_CHUNK_SIZE
];
return PageThumbsWorker.post(
"expireFilesInDirectory",
msg
);
}
};
/**
* Interface to a dedicated thread handling I/O
*/
let PageThumbsWorker = (function() {
let worker = new PromiseWorker("resource://gre/modules/PageThumbsWorker.js",
OS.Shared.LOG.bind("PageThumbs"));
return {
post: function post(...args) {
let promise = worker.post.apply(worker, args);
return promise.then(
null,
function onError(error) {
// Decode any serialized error
if (error instanceof PromiseWorker.WorkerError) {
throw OS.File.Error.fromMsg(error.data);
} else {
throw error;
}
}
);
}
};
})();
let PageThumbsHistoryObserver = {
onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) {
PageThumbsStorage.remove(aURI.spec);
},
onClearHistory: function Thumbnails_onClearHistory() {
PageThumbsStorage.wipe();
},
onTitleChanged: function () {},
onBeginUpdateBatch: function () {},
onEndUpdateBatch: function () {},
onVisit: function () {},
onPageChanged: function () {},
onDeleteVisits: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
};