Bug 861903 - Hook IndexedDB up to low disk space notifications. r=janv.
authorBen Turner <bent.mozilla@gmail.com>
Fri, 10 May 2013 14:22:01 -0700
changeset 142473 22a77fd7ab35cf760481aa2620e737e7109a2b0a
parent 142472 69fcebb240953db14a0501e9814e1c8a04198061
child 142474 519b04170b2f23961ee44d19982bed213e1e2e9f
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanv
bugs861903
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 861903 - Hook IndexedDB up to low disk space notifications. r=janv.
dom/indexedDB/IDBDatabase.cpp
dom/indexedDB/IDBObjectStore.cpp
dom/indexedDB/IndexedDatabaseManager.cpp
dom/indexedDB/IndexedDatabaseManager.h
dom/indexedDB/OpenDatabaseHelper.cpp
dom/indexedDB/test/Makefile.in
dom/indexedDB/test/helpers.js
dom/indexedDB/test/test_lowDiskSpace.html
dom/indexedDB/test/unit/Makefile.in
dom/indexedDB/test/unit/head.js
dom/indexedDB/test/unit/test_lowDiskSpace.js
dom/indexedDB/test/unit/xpcshell.ini
testing/specialpowers/content/specialpowersAPI.js
--- a/dom/indexedDB/IDBDatabase.cpp
+++ b/dom/indexedDB/IDBDatabase.cpp
@@ -900,16 +900,22 @@ NoRequestDatabaseHelper::OnError()
 nsresult
 CreateObjectStoreHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!");
 
   PROFILER_LABEL("IndexedDB", "CreateObjectStoreHelper::DoDatabaseWork");
 
+  if (IndexedDatabaseManager::InLowDiskSpaceMode()) {
+    NS_WARNING("Refusing to create additional objectStore because disk space "
+               "is low!");
+    return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
+  }
+
   nsCOMPtr<mozIStorageStatement> stmt =
     mTransaction->GetCachedStatement(NS_LITERAL_CSTRING(
     "INSERT INTO object_store (id, auto_increment, name, key_path) "
     "VALUES (:id, :auto_increment, :name, :key_path)"
   ));
   NS_ENSURE_TRUE(stmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   mozStorageStatementScoper scoper(stmt);
@@ -980,16 +986,21 @@ DeleteObjectStoreHelper::DoDatabaseWork(
 nsresult
 CreateFileHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!");
 
   PROFILER_LABEL("IndexedDB", "CreateFileHelper::DoDatabaseWork");
 
+  if (IndexedDatabaseManager::InLowDiskSpaceMode()) {
+    NS_WARNING("Refusing to create file because disk space is low!");
+    return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
+  }
+
   FileManager* fileManager = mDatabase->Manager();
 
   mFileInfo = fileManager->GetNewFileInfo();
   NS_ENSURE_TRUE(mFileInfo, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   const int64_t& fileId = mFileInfo->Id();
 
   nsCOMPtr<nsIFile> directory = fileManager->EnsureJournalDirectory();
--- a/dom/indexedDB/IDBObjectStore.cpp
+++ b/dom/indexedDB/IDBObjectStore.cpp
@@ -2940,16 +2940,21 @@ nsresult
 AddHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!");
   NS_ASSERTION(aConnection, "Passed a null connection!");
 
   PROFILER_LABEL("IndexedDB", "AddHelper::DoDatabaseWork");
 
+  if (IndexedDatabaseManager::InLowDiskSpaceMode()) {
+    NS_WARNING("Refusing to add more data because disk space is low!");
+    return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
+  }
+
   nsresult rv;
   bool keyUnset = mKey.IsUnset();
   int64_t osid = mObjectStore->Id();
   const KeyPath& keyPath = mObjectStore->GetKeyPath();
 
   // The "|| keyUnset" here is mostly a debugging tool. If a key isn't
   // specified we should never have a collision and so it shouldn't matter
   // if we allow overwrite or not. By not allowing overwrite we raise
@@ -3909,16 +3914,21 @@ OpenCursorHelper::UnpackResponseFromPare
 nsresult
 CreateIndexHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!");
 
   PROFILER_LABEL("IndexedDB", "CreateIndexHelper::DoDatabaseWork");
 
+  if (IndexedDatabaseManager::InLowDiskSpaceMode()) {
+    NS_WARNING("Refusing to create index because disk space is low!");
+    return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
+  }
+
   // Insert the data into the database.
   nsCOMPtr<mozIStorageStatement> stmt =
     mTransaction->GetCachedStatement(
     "INSERT INTO object_store_index (id, name, key_path, unique_index, "
       "multientry, object_store_id) "
     "VALUES (:id, :name, :key_path, :unique, :multientry, :osid)"
   );
   NS_ENSURE_TRUE(stmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
--- a/dom/indexedDB/IndexedDatabaseManager.cpp
+++ b/dom/indexedDB/IndexedDatabaseManager.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "IndexedDatabaseManager.h"
 
 #include "nsIConsoleService.h"
+#include "nsIDiskSpaceWatcher.h"
 #include "nsIDOMScriptObjectFactory.h"
 #include "nsIFile.h"
 #include "nsIFileStorage.h"
 #include "nsIScriptError.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/quota/Utilities.h"
@@ -21,16 +22,21 @@
 #include "nsEventDispatcher.h"
 #include "nsThreadUtils.h"
 
 #include "IDBEvents.h"
 #include "IDBFactory.h"
 #include "IDBKeyRange.h"
 #include "IDBRequest.h"
 
+// The two possible values for the data argument when receiving the disk space
+// observer notification.
+#define LOW_DISK_SPACE_DATA_FULL "full"
+#define LOW_DISK_SPACE_DATA_FREE "free"
+
 USING_INDEXEDDB_NAMESPACE
 using namespace mozilla::dom;
 USING_QUOTA_NAMESPACE
 
 static NS_DEFINE_CID(kDOMSOF_CID, NS_DOM_SCRIPT_OBJECT_FACTORY_CID);
 
 namespace {
 
@@ -87,31 +93,50 @@ IndexedDatabaseManager::IndexedDatabaseM
 }
 
 IndexedDatabaseManager::~IndexedDatabaseManager()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
 }
 
 bool IndexedDatabaseManager::sIsMainProcess = false;
+int32_t IndexedDatabaseManager::sLowDiskSpaceMode = 0;
 
 // static
 IndexedDatabaseManager*
 IndexedDatabaseManager::GetOrCreate()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
 
   if (IsClosed()) {
     NS_ERROR("Calling GetOrCreate() after shutdown!");
     return nullptr;
   }
 
   if (!gInstance) {
     sIsMainProcess = XRE_GetProcessType() == GeckoProcessType_Default;
 
+    if (sIsMainProcess) {
+      // See if we're starting up in low disk space conditions.
+      nsCOMPtr<nsIDiskSpaceWatcher> watcher =
+        do_GetService(DISKSPACEWATCHER_CONTRACTID);
+      if (watcher) {
+        bool isDiskFull;
+        if (NS_SUCCEEDED(watcher->GetIsDiskFull(&isDiskFull))) {
+          sLowDiskSpaceMode = isDiskFull ? 1 : 0;
+        }
+        else {
+          NS_WARNING("GetIsDiskFull failed!");
+        }
+      }
+      else {
+        NS_WARNING("No disk space watcher component available!");
+      }
+    }
+
     nsRefPtr<IndexedDatabaseManager> instance(new IndexedDatabaseManager());
 
     nsresult rv = instance->Init();
     NS_ENSURE_SUCCESS(rv, nullptr);
 
     if (PR_ATOMIC_SET(&gInitialized, 1)) {
       NS_ERROR("Initialized more than once?!");
     }
@@ -141,23 +166,37 @@ IndexedDatabaseManager::FactoryCreate()
   IndexedDatabaseManager* mgr = GetOrCreate();
   NS_IF_ADDREF(mgr);
   return mgr;
 }
 
 nsresult
 IndexedDatabaseManager::Init()
 {
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+
   // Make sure that the quota manager is up.
-  NS_ENSURE_TRUE(QuotaManager::GetOrCreate(), NS_ERROR_FAILURE);
+  QuotaManager* qm = QuotaManager::GetOrCreate();
+  NS_ENSURE_STATE(qm);
 
-  // Must initialize the storage service on the main thread.
-  nsCOMPtr<mozIStorageService> ss =
-    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
-  NS_ENSURE_TRUE(ss, NS_ERROR_FAILURE);
+  // During Init() we can't yet call IsMainProcess(), just check sIsMainProcess
+  // directly.
+  if (sIsMainProcess) {
+    // Must initialize the storage service on the main thread.
+    nsCOMPtr<mozIStorageService> ss =
+      do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+    NS_ENSURE_STATE(ss);
+
+    nsCOMPtr<nsIObserverService> obs = GetObserverService();
+    NS_ENSURE_STATE(obs);
+
+    nsresult rv =
+      obs->AddObserver(this, DISKSPACEWATCHER_OBSERVER_TOPIC, false);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
 
   return NS_OK;
 }
 
 void
 IndexedDatabaseManager::Destroy()
 {
   // Setting the closed flag prevents the service from being recreated.
@@ -274,26 +313,36 @@ IndexedDatabaseManager::TabContextMayAcc
 // static
 bool
 IndexedDatabaseManager::IsClosed()
 {
   return !!gClosed;
 }
 
 #ifdef DEBUG
-//static
+// static
 bool
 IndexedDatabaseManager::IsMainProcess()
 {
   NS_ASSERTION(gInstance,
                "IsMainProcess() called before indexedDB has been initialized!");
   NS_ASSERTION((XRE_GetProcessType() == GeckoProcessType_Default) ==
                sIsMainProcess, "XRE_GetProcessType changed its tune!");
   return sIsMainProcess;
 }
+
+//static
+bool
+IndexedDatabaseManager::InLowDiskSpaceMode()
+{
+  NS_ASSERTION(gInstance,
+               "InLowDiskSpaceMode() called before indexedDB has been "
+               "initialized!");
+  return !!sLowDiskSpaceMode;
+}
 #endif
 
 already_AddRefed<FileManager>
 IndexedDatabaseManager::GetFileManager(const nsACString& aOrigin,
                                        const nsAString& aDatabaseName)
 {
   nsTArray<nsRefPtr<FileManager> >* array;
   if (!mFileManagers.Get(aOrigin, &array)) {
@@ -389,17 +438,18 @@ IndexedDatabaseManager::AsyncDeleteFile(
     quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 NS_IMPL_ADDREF(IndexedDatabaseManager)
 NS_IMPL_RELEASE_WITH_DESTROY(IndexedDatabaseManager, Destroy())
-NS_IMPL_QUERY_INTERFACE1(IndexedDatabaseManager, nsIIndexedDatabaseManager)
+NS_IMPL_QUERY_INTERFACE2(IndexedDatabaseManager, nsIIndexedDatabaseManager,
+                                                 nsIObserver)
 
 NS_IMETHODIMP
 IndexedDatabaseManager::InitWindowless(const jsval& aObj, JSContext* aCx)
 {
   NS_ENSURE_TRUE(nsContentUtils::IsCallerChrome(), NS_ERROR_NOT_AVAILABLE);
   NS_ENSURE_ARG(!JSVAL_IS_PRIMITIVE(aObj));
 
   JS::Rooted<JSObject*> obj(aCx, JSVAL_TO_OBJECT(aObj));
@@ -449,16 +499,45 @@ IndexedDatabaseManager::InitWindowless(c
   if (!JS_DefineProperty(aCx, obj, "IDBKeyRange", OBJECT_TO_JSVAL(keyrangeObj),
                          nullptr, nullptr, JSPROP_ENUMERATE)) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+IndexedDatabaseManager::Observe(nsISupports* aSubject, const char* aTopic,
+                                const PRUnichar* aData)
+{
+  NS_ASSERTION(IsMainProcess(), "Wrong process!");
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+
+  if (!strcmp(aTopic, LOW_DISK_SPACE_OBSERVER_ID)) {
+    NS_ASSERTION(aData, "No data?!");
+
+    const nsDependentString data(aData);
+
+    if (data.EqualsLiteral(LOW_DISK_SPACE_DATA_FULL)) {
+      PR_ATOMIC_SET(&sLowDiskSpaceMode, 1);
+    }
+    else if (data.EqualsLiteral(LOW_DISK_SPACE_DATA_FREE)) {
+      PR_ATOMIC_SET(&sLowDiskSpaceMode, 0);
+    }
+    else {
+      NS_NOTREACHED("Unknown data value!");
+    }
+
+    return NS_OK;
+  }
+
+   NS_NOTREACHED("Unknown topic!");
+   return NS_ERROR_UNEXPECTED;
+ }
+
 AsyncDeleteFileRunnable::AsyncDeleteFileRunnable(FileManager* aFileManager,
                                                  int64_t aFileId)
 : mFileManager(aFileManager), mFileId(aFileId)
 {
 }
 
 NS_IMPL_THREADSAFE_ISUPPORTS1(AsyncDeleteFileRunnable,
                               nsIRunnable)
--- a/dom/indexedDB/IndexedDatabaseManager.h
+++ b/dom/indexedDB/IndexedDatabaseManager.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_indexeddb_indexeddatabasemanager_h__
 #define mozilla_dom_indexeddb_indexeddatabasemanager_h__
 
 #include "mozilla/dom/indexedDB/IndexedDatabase.h"
 
 #include "nsIIndexedDatabaseManager.h"
+#include "nsIObserver.h"
 
 #include "mozilla/Mutex.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 
 #define INDEXEDDB_MANAGER_CONTRACTID "@mozilla.org/dom/indexeddb/manager;1"
 
 class nsIAtom;
@@ -26,21 +27,23 @@ namespace dom {
 class TabContext;
 }
 }
 
 BEGIN_INDEXEDDB_NAMESPACE
 
 class FileManager;
 
-class IndexedDatabaseManager MOZ_FINAL : public nsIIndexedDatabaseManager
+class IndexedDatabaseManager MOZ_FINAL : public nsIIndexedDatabaseManager,
+                                         public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIINDEXEDDATABASEMANAGER
+  NS_DECL_NSIOBSERVER
 
   // Returns a non-owning reference.
   static IndexedDatabaseManager*
   GetOrCreate();
 
   // Returns a non-owning reference.
   static IndexedDatabaseManager*
   Get();
@@ -57,16 +60,26 @@ public:
 #ifdef DEBUG
   ;
 #else
   {
     return sIsMainProcess;
   }
 #endif
 
+  static bool
+  InLowDiskSpaceMode()
+#ifdef DEBUG
+  ;
+#else
+  {
+    return !!sLowDiskSpaceMode;
+  }
+#endif
+
   already_AddRefed<FileManager>
   GetFileManager(const nsACString& aOrigin,
                  const nsAString& aDatabaseName);
 
   void
   AddFileManager(FileManager* aFileManager);
 
   void
@@ -116,13 +129,14 @@ private:
                    nsTArray<nsRefPtr<FileManager> > > mFileManagers;
 
   // Lock protecting FileManager.mFileInfos and nsDOMFileBase.mFileInfos
   // It's s also used to atomically update FileInfo.mRefCnt, FileInfo.mDBRefCnt
   // and FileInfo.mSliceRefCnt
   mozilla::Mutex mFileMutex;
 
   static bool sIsMainProcess;
+  static int32_t sLowDiskSpaceMode;
 };
 
 END_INDEXEDDB_NAMESPACE
 
 #endif /* mozilla_dom_indexeddb_indexeddatabasemanager_h__ */
--- a/dom/indexedDB/OpenDatabaseHelper.cpp
+++ b/dom/indexedDB/OpenDatabaseHelper.cpp
@@ -1871,40 +1871,51 @@ OpenDatabaseHelper::CreateDatabaseConnec
                                         const nsACString& aOrigin,
                                         mozIStorageConnection** aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!");
 
   PROFILER_LABEL("IndexedDB", "OpenDatabaseHelper::CreateDatabaseConnection");
 
+  nsresult rv;
+  bool exists;
+
+  if (IndexedDatabaseManager::InLowDiskSpaceMode()) {
+    rv = aDBFile->Exists(&exists);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (!exists) {
+      NS_WARNING("Refusing to create database because disk space is low!");
+      return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
+    }
+  }
+
   nsCOMPtr<nsIFileURL> dbFileUrl =
     IDBFactory::GetDatabaseFileURL(aDBFile, aOrigin);
   NS_ENSURE_TRUE(dbFileUrl, NS_ERROR_FAILURE);
 
   nsCOMPtr<mozIStorageService> ss =
     do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
   NS_ENSURE_TRUE(ss, NS_ERROR_FAILURE);
 
   nsCOMPtr<mozIStorageConnection> connection;
-  nsresult rv =
-    ss->OpenDatabaseWithFileURL(dbFileUrl, getter_AddRefs(connection));
+  rv = ss->OpenDatabaseWithFileURL(dbFileUrl, getter_AddRefs(connection));
   if (rv == NS_ERROR_FILE_CORRUPTED) {
     // If we're just opening the database during origin initialization, then
     // we don't want to erase any files. The failure here will fail origin
     // initialization too.
     if (aName.IsVoid()) {
       return rv;
     }
 
     // Nuke the database file.  The web services can recreate their data.
     rv = aDBFile->Remove(false);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    bool exists;
     rv = aFMDirectory->Exists(&exists);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (exists) {
       bool isDirectory;
       rv = aFMDirectory->IsDirectory(&isDirectory);
       NS_ENSURE_SUCCESS(rv, rv);
       NS_ENSURE_TRUE(isDirectory, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
--- a/dom/indexedDB/test/Makefile.in
+++ b/dom/indexedDB/test/Makefile.in
@@ -71,16 +71,17 @@ MOCHITEST_FILES = \
   test_index_update_delete.html \
   test_indexes.html \
   test_indexes_bad_values.html \
   test_indexes_funny_things.html \
   test_invalid_version.html \
   test_key_requirements.html \
   test_keys.html \
   test_leaving_page.html \
+  test_lowDiskSpace.html \
   test_multientry.html \
   test_names_sorted.html \
   test_objectCursors.html \
   test_objectStore_inline_autoincrement_key_added_on_put.html \
   test_objectStore_remove_values.html \
   test_object_identity.html \
   test_odd_result_order.html \
   test_open_empty_db.html \
--- a/dom/indexedDB/test/helpers.js
+++ b/dom/indexedDB/test/helpers.js
@@ -132,16 +132,26 @@ function browserErrorHandler(event)
 }
 
 function unexpectedSuccessHandler()
 {
   ok(false, "Got success, but did not expect it!");
   finishTest();
 }
 
+function expectedErrorHandler(name)
+{
+  return function(event) {
+    is(event.type, "error", "Got an error event");
+    is(event.target.error.name, name, "Expected error was thrown.");
+    event.preventDefault();
+    grabEventAndContinueHandler(event);
+  };
+}
+
 function ExpectError(name, preventDefault)
 {
   this._name = name;
   this._preventDefault = preventDefault;
 }
 ExpectError.prototype = {
   handleEvent: function(event)
   {
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_lowDiskSpace.html
@@ -0,0 +1,19 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>Indexed Database Low Disk Space Test</title>
+
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+  <script type="text/javascript;version=1.7" src="unit/test_lowDiskSpace.js"></script>
+  <script type="text/javascript;version=1.7" src="helpers.js"></script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
--- a/dom/indexedDB/test/unit/Makefile.in
+++ b/dom/indexedDB/test/unit/Makefile.in
@@ -34,16 +34,17 @@ MOCHITEST_FILES = \
   test_index_object_cursors.js \
   test_index_update_delete.js \
   test_indexes.js \
   test_indexes_bad_values.js \
   test_indexes_funny_things.js \
   test_invalid_version.js \
   test_key_requirements.js \
   test_keys.js \
+  test_lowDiskSpace.js \
   test_multientry.js \
   test_names_sorted.js \
   test_object_identity.js \
   test_objectCursors.js \
   test_objectStore_inline_autoincrement_key_added_on_put.js \
   test_objectStore_remove_values.js \
   test_odd_result_order.js \
   test_open_empty_db.js \
--- a/dom/indexedDB/test/unit/head.js
+++ b/dom/indexedDB/test/unit/head.js
@@ -33,16 +33,20 @@ function isnot(a, b, msg) {
 function executeSoon(fun) {
   do_execute_soon(fun);
 }
 
 function todo(condition, name, diag) {
   dump("TODO: ", diag);
 }
 
+function info(msg) {
+  do_print(msg);
+}
+
 function run_test() {
   runTest();
 };
 
 function runTest()
 {
   // XPCShell does not get a profile by default.
   do_get_profile();
@@ -83,16 +87,26 @@ function errorHandler(event)
 }
 
 function unexpectedSuccessHandler()
 {
   do_check_true(false);
   finishTest();
 }
 
+function expectedErrorHandler(name)
+{
+  return function(event) {
+    do_check_eq(event.type, "error");
+    do_check_eq(event.target.error.name, name);
+    event.preventDefault();
+    grabEventAndContinueHandler(event);
+  };
+}
+
 function ExpectError(name)
 {
   this._name = name;
 }
 ExpectError.prototype = {
   handleEvent: function(event)
   {
     do_check_eq(event.type, "error");
@@ -177,10 +191,15 @@ function gc()
   Components.utils.forceCC();
 }
 
 var SpecialPowers = {
   isMainProcess: function() {
     return Components.classes["@mozilla.org/xre/app-info;1"]
                      .getService(Components.interfaces.nsIXULRuntime)
                      .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+  },
+  notifyObservers: function(subject, topic, data) {
+    var obsvc = Cc['@mozilla.org/observer-service;1']
+                   .getService(Ci.nsIObserverService);
+    obsvc.notifyObservers(subject, topic, data);
   }
 };
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_lowDiskSpace.js
@@ -0,0 +1,736 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+var self = this;
+
+var testGenerator = testSteps();
+
+function testSteps()
+{
+  const dbName = self.window ? window.location.pathname : "test_lowDiskSpace";
+  const dbVersion = 1;
+
+  const objectStoreName = "foo";
+  const objectStoreOptions = { keyPath: "foo" };
+
+  const indexName = "bar";
+  const indexOptions = { unique: true };
+
+  const dbData = [
+    { foo: 0, bar: 0 },
+    { foo: 1, bar: 10 },
+    { foo: 2, bar: 20 },
+    { foo: 3, bar: 30 },
+    { foo: 4, bar: 40 },
+    { foo: 5, bar: 50 },
+    { foo: 6, bar: 60 },
+    { foo: 7, bar: 70 },
+    { foo: 8, bar: 80 },
+    { foo: 9, bar: 90 }
+  ];
+
+  let lowDiskMode = false;
+  function setLowDiskMode(val) {
+    let data = val ? "full" : "free";
+
+    if (val == lowDiskMode) {
+      info("Low disk mode is: " + data);
+    }
+    else {
+      info("Changing low disk mode to: " + data);
+      SpecialPowers.notifyObservers(null, "disk-space-watcher", data);
+      lowDiskMode = val;
+    }
+  }
+
+  { // Make sure opening works from the beginning.
+    info("Test 1");
+
+    setLowDiskMode(false);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    is(event.type, "success", "Opened database without setting low disk mode");
+
+    let db = event.target.result;
+    db.close();
+  }
+
+  { // Make sure delete works in low disk mode.
+    info("Test 2");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.deleteDatabase(dbName);
+    request.onerror = errorHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    is(event.type, "success", "Deleted database after setting low disk mode");
+  }
+
+  { // Make sure creating a db in low disk mode fails.
+    info("Test 3");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = expectedErrorHandler("QuotaExceededError");
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+    let event = yield;
+
+    is(event.type, "error", "Didn't create new database in low disk mode");
+  }
+
+  { // Make sure opening an already-existing db in low disk mode succeeds.
+    info("Test 4");
+
+    setLowDiskMode(false);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Created database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+
+    setLowDiskMode(true);
+
+    request = indexedDB.open(dbName);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Opened existing database in low disk mode");
+
+    db = event.target.result;
+    db.close();
+  }
+
+  { // Make sure upgrading an already-existing db in low disk mode succeeds.
+    info("Test 5");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion + 1);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Created database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+  }
+
+  { // Make sure creating objectStores in low disk mode fails.
+    info("Test 6");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion + 2);
+    request.onerror = expectedErrorHandler("QuotaExceededError");
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    event = yield;
+
+    is(event.type, "error", "Failed database upgrade");
+  }
+
+  { // Make sure creating indexes in low disk mode fails.
+    info("Test 7");
+
+    setLowDiskMode(false);
+
+    let request = indexedDB.open(dbName, dbVersion + 2);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Upgraded database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+
+    setLowDiskMode(true);
+
+    request = indexedDB.open(dbName, dbVersion + 3);
+    request.onerror = expectedErrorHandler("QuotaExceededError");
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+    event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    db = event.target.result;
+    db.onerror = errorHandler;
+
+    objectStore = event.target.transaction.objectStore(objectStoreName);
+    let index = objectStore.createIndex(indexName, indexName, indexOptions);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    event = yield;
+
+    is(event.type, "error", "Failed database upgrade");
+  }
+
+  { // Make sure deleting indexes in low disk mode succeeds.
+    info("Test 8");
+
+    setLowDiskMode(false);
+
+    let request = indexedDB.open(dbName, dbVersion + 3);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let objectStore = event.target.transaction.objectStore(objectStoreName);
+    let index = objectStore.createIndex(indexName, indexName, indexOptions);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Upgraded database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+
+    setLowDiskMode(true);
+
+    request = indexedDB.open(dbName, dbVersion + 4);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+    event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    db = event.target.result;
+    db.onerror = errorHandler;
+
+    objectStore = event.target.transaction.objectStore(objectStoreName);
+    objectStore.deleteIndex(indexName);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Upgraded database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+  }
+
+  { // Make sure deleting objectStores in low disk mode succeeds.
+    info("Test 9");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion + 5);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    db.deleteObjectStore(objectStoreName);
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Upgraded database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+
+    // Reset everything.
+    indexedDB.deleteDatabase(dbName);
+  }
+
+
+  { // Add data that the rest of the tests will use.
+    info("Adding test data");
+
+    setLowDiskMode(false);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = grabEventAndContinueHandler;
+    request.onsuccess = unexpectedSuccessHandler;
+    let event = yield;
+
+    is(event.type, "upgradeneeded", "Upgrading database");
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);
+    let index = objectStore.createIndex(indexName, indexName, indexOptions);
+
+    for each (let data in dbData) {
+      objectStore.add(data);
+    }
+
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "success", "Upgraded database");
+    ok(event.target.result === db, "Got the same database");
+
+    db.close();
+  }
+
+  { // Make sure read operations in readonly transactions succeed in low disk
+    // mode.
+    info("Test 10");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let transaction = db.transaction(objectStoreName);
+    let objectStore = transaction.objectStore(objectStoreName);
+    let index = objectStore.index(indexName);
+
+    let data = dbData[0];
+
+    let requestCounter = new RequestCounter();
+
+    objectStore.get(data.foo).onsuccess = requestCounter.handler();
+    objectStore.mozGetAll().onsuccess = requestCounter.handler();
+    objectStore.count().onsuccess = requestCounter.handler();
+    index.get(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAll().onsuccess = requestCounter.handler();
+    index.getKey(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAllKeys().onsuccess = requestCounter.handler();
+    index.count().onsuccess = requestCounter.handler();
+
+    let objectStoreDataCount = 0;
+
+    request = objectStore.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        objectStoreDataCount++;
+        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(objectStoreDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexDataCount++;
+        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexKeyDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexKeyDataCount++;
+        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexKeyDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    // Wait for all requests.
+    yield;
+
+    transaction.oncomplete = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "complete", "Transaction succeeded");
+
+    db.close();
+  }
+
+  { // Make sure read operations in readwrite transactions succeed in low disk
+    // mode.
+    info("Test 11");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let transaction = db.transaction(objectStoreName, "readwrite");
+    let objectStore = transaction.objectStore(objectStoreName);
+    let index = objectStore.index(indexName);
+
+    let data = dbData[0];
+
+    let requestCounter = new RequestCounter();
+
+    objectStore.get(data.foo).onsuccess = requestCounter.handler();
+    objectStore.mozGetAll().onsuccess = requestCounter.handler();
+    objectStore.count().onsuccess = requestCounter.handler();
+    index.get(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAll().onsuccess = requestCounter.handler();
+    index.getKey(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAllKeys().onsuccess = requestCounter.handler();
+    index.count().onsuccess = requestCounter.handler();
+
+    let objectStoreDataCount = 0;
+
+    request = objectStore.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        objectStoreDataCount++;
+        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(objectStoreDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexDataCount++;
+        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexKeyDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexKeyDataCount++;
+        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexKeyDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    // Wait for all requests.
+    yield;
+
+    transaction.oncomplete = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "complete", "Transaction succeeded");
+
+    db.close();
+  }
+
+  { // Make sure write operations in readwrite transactions fail in low disk
+    // mode.
+    info("Test 12");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let transaction = db.transaction(objectStoreName, "readwrite");
+    let objectStore = transaction.objectStore(objectStoreName);
+    let index = objectStore.index(indexName);
+
+    let data = dbData[0];
+    let newData = { foo: 999, bar: 999 };
+
+    let requestCounter = new RequestCounter();
+
+    objectStore.add(newData).onerror = requestCounter.errorHandler();
+    objectStore.put(newData).onerror = requestCounter.errorHandler();
+
+    objectStore.get(data.foo).onsuccess = requestCounter.handler();
+    objectStore.mozGetAll().onsuccess = requestCounter.handler();
+    objectStore.count().onsuccess = requestCounter.handler();
+    index.get(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAll().onsuccess = requestCounter.handler();
+    index.getKey(data.bar).onsuccess = requestCounter.handler();
+    index.mozGetAllKeys().onsuccess = requestCounter.handler();
+    index.count().onsuccess = requestCounter.handler();
+
+    let objectStoreDataCount = 0;
+
+    request = objectStore.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        objectStoreDataCount++;
+        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
+        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(objectStoreDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexDataCount++;
+        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
+        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    let indexKeyDataCount = 0;
+
+    request = index.openCursor();
+    request.onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        indexKeyDataCount++;
+        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
+        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
+      }
+      else {
+        is(indexKeyDataCount, dbData.length, "Saw all data");
+        requestCounter.decr();
+      }
+    };
+    requestCounter.incr();
+
+    // Wait for all requests.
+    yield;
+
+    transaction.oncomplete = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "complete", "Transaction succeeded");
+
+    db.close();
+  }
+
+  { // Make sure deleting operations in readwrite transactions succeed in low
+    // disk mode.
+    info("Test 13");
+
+    setLowDiskMode(true);
+
+    let request = indexedDB.open(dbName, dbVersion);
+    request.onerror = errorHandler;
+    request.onupgradeneeded = unexpectedSuccessHandler;
+    request.onsuccess = grabEventAndContinueHandler;
+    let event = yield;
+
+    let db = event.target.result;
+    db.onerror = errorHandler;
+
+    let transaction = db.transaction(objectStoreName, "readwrite");
+    let objectStore = transaction.objectStore(objectStoreName);
+    let index = objectStore.index(indexName);
+
+    let dataIndex = 0;
+    let data = dbData[dataIndex++];
+
+    let requestCounter = new RequestCounter();
+
+    objectStore.delete(data.foo).onsuccess = requestCounter.handler();
+
+    objectStore.openCursor().onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        cursor.delete().onsuccess = requestCounter.handler();
+      }
+      requestCounter.decr();
+    };
+    requestCounter.incr();
+
+    index.openCursor(null, "prev").onsuccess = function(event) {
+      let cursor = event.target.result;
+      if (cursor) {
+        cursor.delete().onsuccess = requestCounter.handler();
+      }
+      requestCounter.decr();
+    };
+    requestCounter.incr();
+
+    yield;
+
+    objectStore.count().onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.target.result, dbData.length - 3, "Actually deleted something");
+
+    objectStore.clear();
+    objectStore.count().onsuccess = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.target.result, 0, "Actually cleared");
+
+    transaction.oncomplete = grabEventAndContinueHandler;
+    event = yield;
+
+    is(event.type, "complete", "Transaction succeeded");
+
+    db.close();
+  }
+
+  finishTest();
+  yield;
+}
+
+function RequestCounter(expectedType) {
+  this._counter = 0;
+}
+RequestCounter.prototype = {
+  incr: function() {
+    this._counter++;
+  },
+
+  decr: function() {
+    if (!--this._counter) {
+      continueToNextStepSync();
+    }
+  },
+
+  handler: function(type, preventDefault) {
+    this.incr();
+    return function(event) {
+      is(event.type, type || "success", "Correct type");
+      this.decr();
+    }.bind(this);
+  },
+
+  errorHandler: function(eventType, errorName) {
+    this.incr();
+    return function(event) {
+      is(event.type, eventType || "error", "Correct type");
+      is(event.target.error.name, errorName || "QuotaExceededError",
+          "Correct error name");
+      event.preventDefault();
+      event.stopPropagation();
+      this.decr();
+    }.bind(this);
+  }
+};
--- a/dom/indexedDB/test/unit/xpcshell.ini
+++ b/dom/indexedDB/test/unit/xpcshell.ini
@@ -27,16 +27,17 @@ tail =
 [test_index_object_cursors.js]
 [test_index_update_delete.js]
 [test_indexes.js]
 [test_indexes_bad_values.js]
 [test_indexes_funny_things.js]
 [test_invalid_version.js]
 [test_key_requirements.js]
 [test_keys.js]
+[test_lowDiskSpace.js]
 [test_multientry.js]
 [test_names_sorted.js]
 [test_object_identity.js]
 [test_objectCursors.js]
 [test_objectStore_inline_autoincrement_key_added_on_put.js]
 [test_objectStore_remove_values.js]
 [test_odd_result_order.js]
 [test_open_empty_db.js]
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -849,16 +849,21 @@ SpecialPowersAPI.prototype = {
                    .getService(Ci.nsIObserverService);
     obsvc.addObserver(obs, notification, weak);
   },
   removeObserver: function(obs, notification) {
     var obsvc = Cc['@mozilla.org/observer-service;1']
                    .getService(Ci.nsIObserverService);
     obsvc.removeObserver(obs, notification);
   },
+  notifyObservers: function(subject, topic, data) {
+    var obsvc = Cc['@mozilla.org/observer-service;1']
+                   .getService(Ci.nsIObserverService);
+    obsvc.notifyObservers(subject, topic, data);
+  },
 
   can_QI: function(obj) {
     return obj.QueryInterface !== undefined;
   },
   do_QueryInterface: function(obj, iface) {
     return obj.QueryInterface(Ci[iface]);
   },