From 83974652ed54c8d111a99b0f9fe7559d4e3003c1 Mon Sep 17 00:00:00 2001 From: David Rajchenbach-Teller Date: Thu, 3 May 2012 16:56:33 +0200 Subject: [PATCH] Bug 722332: add an asynchronous initialization API to nsIBrowserSearchService, with a fallback to synchronous initialization for backwards compatibility, r=gavin --- .../base/public/nsIBrowserSearchService.idl | 41 +++++- toolkit/components/search/nsSearchService.js | 122 ++++++++++++++++-- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/netwerk/base/public/nsIBrowserSearchService.idl b/netwerk/base/public/nsIBrowserSearchService.idl index eee9ae3dc2ce..6bb48b6301c4 100644 --- a/netwerk/base/public/nsIBrowserSearchService.idl +++ b/netwerk/base/public/nsIBrowserSearchService.idl @@ -128,9 +128,48 @@ interface nsISearchEngine : nsISupports }; -[scriptable, uuid(8307b8f2-08ea-45b8-96bf-b1dc7688fe3b)] +/** + * Callback for asynchronous initialization of nsIBrowserSearchService + */ +[scriptable, function, uuid(02256156-16e4-47f1-9979-76ff98ceb590)] +interface nsIBrowserSearchInitObserver : nsISupports +{ + /** + * Called once initialization of the browser search service is complete. + * + * @param aStatus The status of that service. + */ + void onInitComplete(in nsresult aStatus); +}; + +[scriptable, uuid(a676eb70-6987-4429-a668-3e253b6f7c7c)] interface nsIBrowserSearchService : nsISupports { + /** + * Start asynchronous initialization. + * + * The callback is triggered once initialization is complete, which may be + * immediately, if initialization has already been completed by some previous + * call to this method. The callback is always invoked asynchronously. + * + * @param aObserver An optional object observing the end of initialization. + */ + void init([optional] in nsIBrowserSearchInitObserver aObserver); + + /** + * Determine whether initialization has been completed. + * + * Clients of the service can use this attribute to quickly determine whether + * initialization is complete, and decide to trigger some immediate treatment, + * to launch asynchronous initialization or to bailout. + * + * Note that this attribute does not indicate that initialization has succeeded. + * + * @return |true| if the search service is now initialized, |false| if + * initialization has not been triggered yet. + */ + readonly attribute bool isInitialized; + /** * Adds a new search engine from the file at the supplied URI, optionally * asking the user for confirmation first. If a confirmation dialog is diff --git a/toolkit/components/search/nsSearchService.js b/toolkit/components/search/nsSearchService.js index 7b7289864f70..eaa809d3800a 100644 --- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -734,9 +734,9 @@ function getMozParamPref(prefName) * * @see nsIBrowserSearchService.idl */ -let gEnginesLoaded = false; +let gInitialized = false; function notifyAction(aEngine, aVerb) { - if (gEnginesLoaded) { + if (gInitialized) { LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\""); Services.obs.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb); } @@ -2466,23 +2466,85 @@ Submission.prototype = { } } +function executeSoon(func) { + Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL); +} + // nsIBrowserSearchService function SearchService() { // Replace empty LOG function with the useful one if the log pref is set. if (getBoolPref(BROWSER_SEARCH_PREF + "log", false)) LOG = DO_LOG; - try { - this._loadEngines(); - } catch (ex) { - LOG("_init: failure loading engines: " + ex); - } - gEnginesLoaded = true; - this._addObservers(); + /** + * If initialization is not complete yet, an array of + * |nsIBrowserSearchInitObserver| expecting the result of the end of + * initialization. + * Once initialization is complete, |null|. + */ + this._initObservers = []; } + SearchService.prototype = { classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"), + // The current status of initialization. Note that it does not determine if + // initialization is complete, only if an error has been encountered so far. + _initRV: Cr.NS_OK, + + // If initialization has not been completed yet, perform synchronous + // initialization. + // Throws in case of initialization error. + _ensureInitialized: function SRCH_SVC__ensureInitialized() { + if (gInitialized) { + if (!Components.isSuccessCode(this._initRV)) { + throw this._initRV; + } + + // Ensure that the following calls to |_ensureInitialized| can be inlined + // to a noop. Note that we could do this at the end of both |_init| and + // |_syncInit|, to save one call to a non-empty |_ensureInitialized|, but + // this would complicate code. + delete this._ensureInitialized; + this._ensureInitialized = function SRCH_SVC__ensureInitializedDone() { }; + return; + } + + let warning = "Search service falling back to synchronous initialization at " + new Error().stack; + Components.utils.reportError(warning); + LOG(warning); + + this._syncInit(); + if (!Components.isSuccessCode(this._initRV)) { + throw this._initRV; + } + }, + + // Synchronous implementation of the initializer. + // Used as by |_ensureInitialized| as a fallback if initialization is not + // complete. In this implementation, it is also used by |init|. + _syncInit: function SRCH_SVC__syncInit() { + try { + this._loadEngines(); + } catch (ex) { + this._initRV = Cr.NS_ERROR_FAILURE; + LOG("_syncInit: failure loading engines: " + ex); + } + this._addObservers(); + + gInitialized = true; + + // Notify all of the init observers + this._initObservers.forEach(function (observer) { + try { + observer.onInitComplete(this._initRV); + } catch (x) { + LOG("nsIBrowserInitObserver failed with error " + x); + } + }, this); + this._initObservers = null; + }, + _engines: { }, __sortedEngines: null, get _sortedEngines() { @@ -3151,7 +3213,36 @@ SearchService.prototype = { }, // nsIBrowserSearchService + init: function SRCH_SVC_init(observer) { + if (gInitialized) { + if (observer) { + executeSoon(function () { + observer.onInitComplete(this._initRV); + }); + } + return; + } + + if (observer) + this._initObservers.push(observer); + + if (!this._initStarted) { + executeSoon((function () { + // Someone may have since called syncInit via ensureInitialized - if so, + // nothing to do here. + if (!gInitialized) + this._syncInit(); + }).bind(this)); + this._initStarted = true; + } + }, + + get isInitialized() { + return gInitialized; + }, + getEngines: function SRCH_SVC_getEngines(aCount) { + this._ensureInitialized(); LOG("getEngines: getting all engines"); var engines = this._getSortedEngines(true); aCount.value = engines.length; @@ -3159,6 +3250,7 @@ SearchService.prototype = { }, getVisibleEngines: function SRCH_SVC_getVisible(aCount) { + this._ensureInitialized(); LOG("getVisibleEngines: getting all visible engines"); var engines = this._getSortedEngines(false); aCount.value = engines.length; @@ -3166,6 +3258,7 @@ SearchService.prototype = { }, getDefaultEngines: function SRCH_SVC_getDefault(aCount) { + this._ensureInitialized(); function isDefault(engine) { return engine._isDefault; }; @@ -3224,10 +3317,12 @@ SearchService.prototype = { }, getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) { + this._ensureInitialized(); return this._engines[aEngineName] || null; }, getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) { + this._ensureInitialized(); for (var engineName in this._engines) { var engine = this._engines[engineName]; if (engine && engine.alias == aAlias) @@ -3239,6 +3334,7 @@ SearchService.prototype = { addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias, aDescription, aMethod, aTemplate) { + this._ensureInitialized(); if (!aName) FAIL("Invalid name passed to addEngineWithDetails!"); if (!aMethod) @@ -3258,6 +3354,7 @@ SearchService.prototype = { addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL, aConfirm) { LOG("addEngine: Adding \"" + aEngineURL + "\"."); + this._ensureInitialized(); try { var uri = makeURI(aEngineURL); var engine = new Engine(uri, aDataType, false); @@ -3270,6 +3367,7 @@ SearchService.prototype = { }, removeEngine: function SRCH_SVC_removeEngine(aEngine) { + this._ensureInitialized(); if (!aEngine) FAIL("no engine passed to removeEngine!"); @@ -3317,6 +3415,7 @@ SearchService.prototype = { }, moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) { + this._ensureInitialized(); if ((aNewIndex > this._sortedEngines.length) || (aNewIndex < 0)) FAIL("SRCH_SVC_moveEngine: Index out of bounds!"); if (!(aEngine instanceof Ci.nsISearchEngine)) @@ -3366,6 +3465,7 @@ SearchService.prototype = { }, restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() { + this._ensureInitialized(); for each (var e in this._engines) { // Unhide all default engines if (e.hidden && e._isDefault) @@ -3374,11 +3474,13 @@ SearchService.prototype = { }, get originalDefaultEngine() { + this._ensureInitialized(); const defPref = BROWSER_SEARCH_PREF + "defaultenginename"; return this.getEngineByName(getLocalizedPref(defPref, "")); }, get defaultEngine() { + this._ensureInitialized(); let defaultEngine = this.originalDefaultEngine; if (!defaultEngine || defaultEngine.hidden) defaultEngine = this._getSortedEngines(false)[0] || null; @@ -3386,6 +3488,7 @@ SearchService.prototype = { }, get currentEngine() { + this._ensureInitialized(); if (!this._currentEngine) { let selectedEngine = getLocalizedPref(BROWSER_SEARCH_PREF + "selectedEngine"); @@ -3397,6 +3500,7 @@ SearchService.prototype = { return this._currentEngine; }, set currentEngine(val) { + this._ensureInitialized(); if (!(val instanceof Ci.nsISearchEngine)) FAIL("Invalid argument passed to currentEngine setter");