Bug 731494 - Refactor generic code from services/sync into services/common; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Thu, 05 Apr 2012 23:26:06 -0700
changeset 93194 579f1d93491c059a42ee7a36f0c7234adf2fc5c3
parent 92888 da0d07b5ca1e79037eb66bad22bd2b3aeb106dc8
child 93195 b2cb9c43964e6cee8ad2e2118ccbbe65158c6dd6
push id1127
push userlsblakk@mozilla.com
push dateTue, 24 Apr 2012 17:50:26 +0000
treeherdermozilla-aurora@13580e09e879 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs731494
milestone14.0a1
Bug 731494 - Refactor generic code from services/sync into services/common; r=rnewman
browser/base/content/sync/notification.xml
browser/installer/removed-files.in
services/Makefile.in
services/common/Makefile.in
services/common/async.js
services/common/log4moz.js
services/common/observers.js
services/common/preferences.js
services/common/rest.js
services/common/services-common.js
services/common/stringbundle.js
services/common/tests/Makefile.in
services/common/tests/unit/head_global.js
services/common/tests/unit/head_helpers.js
services/common/tests/unit/test_async_chain.js
services/common/tests/unit/test_async_querySpinningly.js
services/common/tests/unit/test_load_modules.js
services/common/tests/unit/test_log4moz.js
services/common/tests/unit/test_observers.js
services/common/tests/unit/test_preferences.js
services/common/tests/unit/test_restrequest.js
services/common/tests/unit/test_utils_makeURI.js
services/common/tests/unit/test_utils_namedTimer.js
services/common/tests/unit/test_utils_stackTrace.js
services/common/tests/unit/xpcshell.ini
services/common/utils.js
services/makefiles.sh
services/sync/SyncComponents.manifest
services/sync/modules/addonsreconciler.js
services/sync/modules/async.js
services/sync/modules/engines.js
services/sync/modules/engines/addons.js
services/sync/modules/engines/bookmarks.js
services/sync/modules/engines/clients.js
services/sync/modules/engines/forms.js
services/sync/modules/engines/history.js
services/sync/modules/engines/prefs.js
services/sync/modules/engines/tabs.js
services/sync/modules/ext/Observers.js
services/sync/modules/ext/Preferences.js
services/sync/modules/ext/StringBundle.js
services/sync/modules/identity.js
services/sync/modules/jpakeclient.js
services/sync/modules/keys.js
services/sync/modules/log4moz.js
services/sync/modules/notifications.js
services/sync/modules/policies.js
services/sync/modules/record.js
services/sync/modules/resource.js
services/sync/modules/rest.js
services/sync/modules/service.js
services/sync/modules/status.js
services/sync/modules/util.js
services/sync/tests/unit/head_appinfo.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_Observers.js
services/sync/tests/unit/test_Preferences.js
services/sync/tests/unit/test_addons_engine.js
services/sync/tests/unit/test_addons_store.js
services/sync/tests/unit/test_async_chain.js
services/sync/tests/unit/test_async_querySpinningly.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
services/sync/tests/unit/test_bookmark_livemarks.js
services/sync/tests/unit/test_bookmark_record.js
services/sync/tests/unit/test_bookmark_smart_bookmarks.js
services/sync/tests/unit/test_corrupt_keys.js
services/sync/tests/unit/test_engine.js
services/sync/tests/unit/test_errorhandler_filelog.js
services/sync/tests/unit/test_forms_tracker.js
services/sync/tests/unit/test_history_store.js
services/sync/tests/unit/test_httpd_sync_server.js
services/sync/tests/unit/test_jpakeclient.js
services/sync/tests/unit/test_load_modules.js
services/sync/tests/unit/test_log4moz.js
services/sync/tests/unit/test_node_reassignment.js
services/sync/tests/unit/test_places_guid_downgrade.js
services/sync/tests/unit/test_prefs_store.js
services/sync/tests/unit/test_prefs_tracker.js
services/sync/tests/unit/test_records_crypto.js
services/sync/tests/unit/test_resource.js
services/sync/tests/unit/test_resource_async.js
services/sync/tests/unit/test_restrequest.js
services/sync/tests/unit/test_service_changePassword.js
services/sync/tests/unit/test_service_detect_upgrade.js
services/sync/tests/unit/test_service_getStorageInfo.js
services/sync/tests/unit/test_service_login.js
services/sync/tests/unit/test_service_migratePrefs.js
services/sync/tests/unit/test_service_startup.js
services/sync/tests/unit/test_service_sync_remoteSetup.js
services/sync/tests/unit/test_service_verifyLogin.js
services/sync/tests/unit/test_syncstoragerequest.js
services/sync/tests/unit/test_utils_lazyStrings.js
services/sync/tests/unit/test_utils_makeURI.js
services/sync/tests/unit/test_utils_namedTimer.js
services/sync/tests/unit/test_utils_stackTrace.js
services/sync/tests/unit/xpcshell.ini
services/sync/tps/extensions/tps/modules/addons.jsm
services/sync/tps/extensions/tps/modules/bookmarks.jsm
services/sync/tps/extensions/tps/modules/history.jsm
services/sync/tps/extensions/tps/modules/tps.jsm
testing/xpcshell/xpcshell.ini
--- a/browser/base/content/sync/notification.xml
+++ b/browser/base/content/sync/notification.xml
@@ -52,27 +52,27 @@
         <children includes="notification"/>
       </xul:vbox>
       <children/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.add("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.add("weave:notification:removed", this.onNotificationRemoved, this);
 
         for each (var notification in Weave.Notifications.notifications)
           this._appendNotification(notification);
       ]]></constructor>
 
       <destructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.remove("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this);
       ]]></destructor>
 
       <method name="onNotificationAdded">
         <parameter name="subject"/>
         <parameter name="data"/>
         <body><![CDATA[
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -1026,16 +1026,23 @@ xpicleanup@BIN_SUFFIX@
   modules/PlacesUIUtils.jsm
   modules/PlacesUtils.jsm
   modules/PluginProvider.jsm
   modules/PluralForm.jsm
   modules/PopupNotifications.jsm
   modules/PropertyPanel.jsm
   modules/reflect.jsm
   modules/Services.jsm
+  modules/services-common/async.js
+  modules/services-common/log4moz.js
+  modules/services-common/observers.js
+  modules/services-common/preferences.js
+  modules/services-common/rest.js
+  modules/services-common/stringbundle.js
+  modules/services-common/utils.js
   modules/services-sync/auth.js
   modules/services-sync/base_records/collection.js
   modules/services-sync/base_records/crypto.js
   modules/services-sync/base_records/keys.js
   modules/services-sync/base_records/wbo.js
   modules/services-sync/constants.js
   modules/services-sync/engines/bookmarks.js
   modules/services-sync/engines/clients.js
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -38,12 +38,12 @@
 DEPTH     = ..
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 ifdef MOZ_SERVICES_SYNC
-PARALLEL_DIRS += crypto sync
+PARALLEL_DIRS += common crypto sync
 endif
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/Makefile.in
@@ -0,0 +1,33 @@
+# 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/.
+
+DEPTH     = ../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+PREF_JS_EXPORTS = $(srcdir)/services-common.js
+
+modules := \
+  async.js \
+  log4moz.js \
+  observers.js \
+  preferences.js \
+  rest.js \
+  stringbundle.js \
+  utils.js \
+  $(NULL)
+
+source_modules = $(foreach module,$(modules),$(srcdir)/$(module))
+module_dir = $(FINAL_TARGET)/modules/services-common
+
+libs::
+	$(NSINSTALL) -D $(module_dir)
+	$(NSINSTALL) -l $(source_modules) $(module_dir)
+
+TEST_DIRS += tests
+
+include $(topsrcdir)/config/rules.mk
rename from services/sync/modules/async.js
rename to services/common/async.js
--- a/services/sync/modules/async.js
+++ b/services/common/async.js
@@ -1,51 +1,15 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Firefox Sync.
- *
- * The Initial Developer of the Original Code is
- * the Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  Richard Newman <rnewman@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* 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/. */
 
-const EXPORTED_SYMBOLS = ['Async'];
+const EXPORTED_SYMBOLS = ["Async"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 // Constants for makeSyncCallback, waitForSyncCallback.
 const CB_READY = {};
 const CB_COMPLETE = {};
 const CB_FAIL = {};
 
 const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR;
 
@@ -56,20 +20,20 @@ Cu.import("resource://gre/modules/Servic
  */
 let Async = {
 
   /**
    * Execute an arbitrary number of asynchronous functions one after the
    * other, passing the callback arguments on to the next one.  All functions
    * must take a callback function as their last argument.  The 'this' object
    * will be whatever chain()'s is.
-   * 
+   *
    * @usage this._chain = Async.chain;
    *        this._chain(this.foo, this.bar, this.baz)(args, for, foo)
-   * 
+   *
    * This is equivalent to:
    *
    *   let self = this;
    *   self.foo(args, for, foo, function (bars, args) {
    *     self.bar(bars, args, function (baz, params) {
    *       self.baz(baz, params);
    *     });
    *   });
rename from services/sync/modules/log4moz.js
rename to services/common/log4moz.js
--- a/services/sync/modules/log4moz.js
+++ b/services/common/log4moz.js
@@ -34,20 +34,17 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ['Log4Moz'];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 const ONE_BYTE = 1;
 const ONE_KILOBYTE = 1024 * ONE_BYTE;
 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
 
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
@@ -368,17 +365,17 @@ Formatter.prototype = {
 function BasicFormatter(dateFormat) {
   if (dateFormat)
     this.dateFormat = dateFormat;
 }
 BasicFormatter.prototype = {
   __proto__: Formatter.prototype,
 
   format: function BF_format(message) {
-    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc 
+    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc
            + "\t" + message.message + "\n";
   }
 };
 
 /*
  * Appenders
  * These can be attached to Loggers to log to different places
  * Simply subclass and override doAppend to implement a new one
@@ -437,17 +434,17 @@ ConsoleAppender.prototype = {
     }
     Cc["@mozilla.org/consoleservice;1"].
       getService(Ci.nsIConsoleService).logStringMessage(message);
   }
 };
 
 /**
  * Base implementation for stream based appenders.
- * 
+ *
  * Caution: This writes to the output stream synchronously, thus logging calls
  * block as the data is written to the stream. This can have negligible impact
  * for in-memory streams, but should be taken into account for I/O streams
  * (files, network, etc.)
  */
 function BlockingStreamAppender(formatter) {
   this._name = "BlockingStreamAppender";
   Appender.call(this, formatter);
@@ -455,17 +452,17 @@ function BlockingStreamAppender(formatte
 BlockingStreamAppender.prototype = {
   __proto__: Appender.prototype,
 
   _converterStream: null, // holds the nsIConverterOutputStream
   _outputStream: null,    // holds the underlying nsIOutputStream
 
   /**
    * Output stream to write to.
-   * 
+   *
    * This will automatically open the stream if it doesn't exist yet by
    * calling newOutputStream. The resulting raw stream is wrapped in a
    * nsIConverterOutputStream to ensure text is written as UTF-8.
    */
   get outputStream() {
     if (!this._outputStream) {
       // First create a raw stream. We can bail out early if that fails.
       this._outputStream = this.newOutputStream();
@@ -476,17 +473,17 @@ BlockingStreamAppender.prototype = {
       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
       // the instance if we already have one.
       if (!this._converterStream) {
         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                                   .createInstance(Ci.nsIConverterOutputStream);
       }
       this._converterStream.init(
         this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
-        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);      
+        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
     }
     return this._converterStream;
   },
 
   newOutputStream: function newOutputStream() {
     throw "Stream-based appenders need to implement newOutputStream()!";
   },
 
@@ -516,27 +513,27 @@ BlockingStreamAppender.prototype = {
         }
       }
     }
   }
 };
 
 /**
  * Append to an nsIStorageStream
- * 
+ *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
  * during logging. Instead, one can periodically consume the input stream and
  * e.g. write it to disk asynchronously.
  */
 function StorageStreamAppender(formatter) {
   this._name = "StorageStreamAppender";
   BlockingStreamAppender.call(this, formatter);
 }
-StorageStreamAppender.prototype = { 
+StorageStreamAppender.prototype = {
   __proto__: BlockingStreamAppender.prototype,
 
   _ss: null,
   newOutputStream: function newOutputStream() {
     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
                           .createInstance(Ci.nsIStorageStream);
     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
     return ss.getOutputStream(0);
@@ -585,17 +582,17 @@ FileAppender.prototype = {
     } catch (e) {
       // File didn't exist in the first place, or we're on Windows. Meh.
     }
   }
 };
 
 /**
  * Rotating file appender (discouraged)
- * 
+ *
  * Similar to FileAppender, but rotates logs when they become too large.
  */
 function RotatingFileAppender(file, formatter, maxSize, maxBackups) {
   if (maxSize === undefined)
     maxSize = ONE_MEGABYTE * 2;
 
   if (maxBackups === undefined)
     maxBackups = 0;
rename from services/sync/modules/ext/Observers.js
rename to services/common/observers.js
rename from services/sync/modules/ext/Preferences.js
rename to services/common/preferences.js
new file mode 100644
--- /dev/null
+++ b/services/common/rest.js
@@ -0,0 +1,578 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["RESTRequest"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/utils.js");
+
+const Prefs = new Preferences("services.common.rest.");
+
+/**
+ * Single use HTTP requests to RESTish resources.
+ *
+ * @param uri
+ *        URI for the request. This can be an nsIURI object or a string
+ *        that can be used to create one. An exception will be thrown if
+ *        the string is not a valid URI.
+ *
+ * Examples:
+ *
+ * (1) Quick GET request:
+ *
+ *   new RESTRequest("http://server/rest/resource").get(function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       processHTTPError(this.response.status);
+ *       return;
+ *     }
+ *     processData(this.response.body);
+ *   });
+ *
+ * (2) Quick PUT request (non-string data is automatically JSONified)
+ *
+ *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
+ *     ...
+ *   });
+ *
+ * (3) Streaming GET
+ *
+ *   let request = new RESTRequest("http://server/rest/resource");
+ *   request.setHeader("Accept", "application/newlines");
+ *   request.onComplete = function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     callbackAfterRequestHasCompleted()
+ *   });
+ *   request.onProgress = function () {
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       return;
+ *     }
+ *     // Process body data and reset it so we don't process the same data twice.
+ *     processIncrementalData(this.response.body);
+ *     this.response.body = "";
+ *   });
+ *   request.get();
+ */
+function RESTRequest(uri) {
+  this.status = this.NOT_SENT;
+
+  // If we don't have an nsIURI object yet, make one. This will throw if
+  // 'uri' isn't a valid URI string.
+  if (!(uri instanceof Ci.nsIURI)) {
+    uri = Services.io.newURI(uri, null, null);
+  }
+  this.uri = uri;
+
+  this._headers = {};
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.request")];
+}
+RESTRequest.prototype = {
+
+  _logName: "Services.Common.RESTRequest",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIBadCertListener2,
+    Ci.nsIInterfaceRequestor,
+    Ci.nsIChannelEventSink
+  ]),
+
+  /*** Public API: ***/
+
+  /**
+   * URI for the request (an nsIURI object).
+   */
+  uri: null,
+
+  /**
+   * HTTP method (e.g. "GET")
+   */
+  method: null,
+
+  /**
+   * RESTResponse object
+   */
+  response: null,
+
+  /**
+   * nsIRequest load flags. Don't do any caching by default.
+   */
+  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
+
+  /**
+   * nsIHttpChannel
+   */
+  channel: null,
+
+  /**
+   * Flag to indicate the status of the request.
+   *
+   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
+   */
+  status: null,
+
+  NOT_SENT:    0,
+  SENT:        1,
+  IN_PROGRESS: 2,
+  COMPLETED:   4,
+  ABORTED:     8,
+
+  /**
+   * Request timeout (in seconds, though decimal values can be used for
+   * up to millisecond granularity.)
+   *
+   * 0 for no timeout.
+   */
+  timeout: null,
+
+  /**
+   * Called when the request has been completed, including failures and
+   * timeouts.
+   *
+   * @param error
+   *        Error that occurred while making the request, null if there
+   *        was no error.
+   */
+  onComplete: function onComplete(error) {
+  },
+
+  /**
+   * Called whenever data is being received on the channel. If this throws an
+   * exception, the request is aborted and the exception is passed as the
+   * error to onComplete().
+   */
+  onProgress: function onProgress() {
+  },
+
+  /**
+   * Set a request header.
+   */
+  setHeader: function setHeader(name, value) {
+    this._headers[name.toLowerCase()] = value;
+  },
+
+  /**
+   * Perform an HTTP GET.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  get: function get(onComplete, onProgress) {
+    return this.dispatch("GET", null, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP PUT.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  put: function put(data, onComplete, onProgress) {
+    return this.dispatch("PUT", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP POST.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  post: function post(data, onComplete, onProgress) {
+    return this.dispatch("POST", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP DELETE.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  delete: function delete_(onComplete, onProgress) {
+    return this.dispatch("DELETE", null, onComplete, onProgress);
+  },
+
+  /**
+   * Abort an active request.
+   */
+  abort: function abort() {
+    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
+      throw "Can only abort a request that has been sent.";
+    }
+
+    this.status = this.ABORTED;
+    this.channel.cancel(Cr.NS_BINDING_ABORTED);
+
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+  },
+
+  /*** Implementation stuff ***/
+
+  dispatch: function dispatch(method, data, onComplete, onProgress) {
+    if (this.status != this.NOT_SENT) {
+      throw "Request has already been sent!";
+    }
+
+    this.method = method;
+    if (onComplete) {
+      this.onComplete = onComplete;
+    }
+    if (onProgress) {
+      this.onProgress = onProgress;
+    }
+
+    // Create and initialize HTTP channel.
+    let channel = Services.io.newChannelFromURI(this.uri, null, null)
+                          .QueryInterface(Ci.nsIRequest)
+                          .QueryInterface(Ci.nsIHttpChannel);
+    this.channel = channel;
+    channel.loadFlags |= this.loadFlags;
+    channel.notificationCallbacks = this;
+
+    // Set request headers.
+    let headers = this._headers;
+    for (let key in headers) {
+      if (key == 'authorization') {
+        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
+      } else {
+        this._log.trace("HTTP Header " + key + ": " + headers[key]);
+      }
+      channel.setRequestHeader(key, headers[key], false);
+    }
+
+    // Set HTTP request body.
+    if (method == "PUT" || method == "POST") {
+      // Convert non-string bodies into JSON.
+      if (typeof data != "string") {
+        data = JSON.stringify(data);
+      }
+
+      this._log.debug(method + " Length: " + data.length);
+      if (this._log.level <= Log4Moz.Level.Trace) {
+        this._log.trace(method + " Body: " + data);
+      }
+
+      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+                     .createInstance(Ci.nsIStringInputStream);
+      stream.setData(data, data.length);
+
+      let type = headers["content-type"] || "text/plain";
+      channel.QueryInterface(Ci.nsIUploadChannel);
+      channel.setUploadStream(stream, type, data.length);
+    }
+    // We must set this after setting the upload stream, otherwise it
+    // will always be 'PUT'. Yeah, I know.
+    channel.requestMethod = method;
+
+    // Blast off!
+    channel.asyncOpen(this, null);
+    this.status = this.SENT;
+    this.delayTimeout();
+    return this;
+  },
+
+  /**
+   * Create or push back the abort timer that kills this request.
+   */
+  delayTimeout: function delayTimeout() {
+    if (this.timeout) {
+      CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
+                             "timeoutTimer");
+    }
+  },
+
+  /**
+   * Abort the request based on a timeout.
+   */
+  abortTimeout: function abortTimeout() {
+    this.abort();
+    let error = Components.Exception("Aborting due to channel inactivity.",
+                                     Cr.NS_ERROR_NET_TIMEOUT);
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortTimeout.")
+      return;
+    }
+    this.onComplete(error);
+  },
+
+  /*** nsIStreamListener ***/
+
+  onStartRequest: function onStartRequest(channel) {
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
+      this.status = this.ABORTED;
+      channel.cancel(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
+    this.status = this.IN_PROGRESS;
+
+    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
+                    channel.URI.spec);
+
+    // Create a response object and fill it with some data.
+    let response = this.response = new RESTResponse();
+    response.request = this;
+    response.body = "";
+
+    // Define this here so that we don't have make a new one each time
+    // onDataAvailable() gets called.
+    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                          .createInstance(Ci.nsIScriptableInputStream);
+
+    this.delayTimeout();
+  },
+
+  onStopRequest: function onStopRequest(channel, context, statusCode) {
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+
+    // We don't want to do anything for a request that's already been aborted.
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      this.status = this.ABORTED;
+      return;
+    }
+    this.status = this.COMPLETED;
+
+    let statusSuccess = Components.isSuccessCode(statusCode);
+    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
+    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
+                    " returned status code " + statusCode);
+
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortRequest.");
+      this.onProgress = null;
+      return;
+    }
+
+    // Throw the failure code and stop execution.  Use Components.Exception()
+    // instead of Error() so the exception is QI-able and can be passed across
+    // XPCOM borders while preserving the status code.
+    if (!statusSuccess) {
+      let message = Components.Exception("", statusCode).name;
+      let error = Components.Exception(message, statusCode);
+      this.onComplete(error);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this._log.debug(this.method + " " + uri + " " + this.response.status);
+
+    // Additionally give the full response body when Trace logging.
+    if (this._log.level <= Log4Moz.Level.Trace) {
+      this._log.trace(this.method + " body: " + this.response.body);
+    }
+
+    delete this._inputStream;
+
+    this.onComplete(null);
+    this.onComplete = this.onProgress = null;
+  },
+
+  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
+    this._inputStream.init(stream);
+    try {
+      this.response.body += this._inputStream.read(count);
+    } catch (ex) {
+      this._log.warn("Exception thrown reading " + count +
+                     " bytes from the channel.");
+      this._log.debug(CommonUtils.exceptionStr(ex));
+      throw ex;
+    }
+
+    try {
+      this.onProgress();
+    } catch (ex) {
+      this._log.warn("Got exception calling onProgress handler, aborting " +
+                     this.method + " " + req.URI.spec);
+      this._log.debug("Exception: " + CommonUtils.exceptionStr(ex));
+      this.abort();
+
+      if (!this.onComplete) {
+        this._log.error("Unexpected error: onComplete not defined in " +
+                        "onDataAvailable.");
+        this.onProgress = null;
+        return;
+      }
+
+      this.onComplete(ex);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this.delayTimeout();
+  },
+
+  /*** nsIInterfaceRequestor ***/
+
+  getInterface: function(aIID) {
+    return this.QueryInterface(aIID);
+  },
+
+  /*** nsIBadCertListener2 ***/
+
+  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
+    this._log.warn("Invalid HTTPS certificate encountered!");
+    // Suppress invalid HTTPS certificate warnings in the UI.
+    // (The request will still fail.)
+    return true;
+  },
+
+  /*** nsIChannelEventSink ***/
+  asyncOnChannelRedirect:
+    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+
+    try {
+      newChannel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
+      return;
+    }
+
+    this.channel = newChannel;
+
+    // We let all redirects proceed.
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  }
+};
+
+/**
+ * Response object for a RESTRequest. This will be created automatically by
+ * the RESTRequest.
+ */
+function RESTResponse() {
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.response")];
+}
+RESTResponse.prototype = {
+
+  _logName: "Sync.RESTResponse",
+
+  /**
+   * Corresponding REST request
+   */
+  request: null,
+
+  /**
+   * HTTP status code
+   */
+  get status() {
+    let status;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      status = channel.responseStatus;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP status code:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.status;
+    return this.status = status;
+  },
+
+  /**
+   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
+   */
+  get success() {
+    let success;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      success = channel.requestSucceeded;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP success flag:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.success;
+    return this.success = success;
+  },
+
+  /**
+   * Object containing HTTP headers (keyed as lower case)
+   */
+  get headers() {
+    let headers = {};
+    try {
+      this._log.trace("Processing response headers.");
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      channel.visitResponseHeaders(function (header, value) {
+        headers[header.toLowerCase()] = value;
+      });
+    } catch (ex) {
+      this._log.debug("Caught exception processing response headers:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+
+    delete this.headers;
+    return this.headers = headers;
+  },
+
+  /**
+   * HTTP body (string)
+   */
+  body: null
+
+};
new file mode 100644
--- /dev/null
+++ b/services/common/services-common.js
@@ -0,0 +1,5 @@
+// This file contains default preference values for components in
+// services-common.
+
+pref("services.common.log.logger.rest.request", "Debug");
+pref("services.common.log.logger.rest.response", "Debug");
rename from services/sync/modules/ext/StringBundle.js
rename to services/common/stringbundle.js
--- a/services/sync/modules/ext/StringBundle.js
+++ b/services/common/stringbundle.js
@@ -29,22 +29,19 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-let EXPORTED_SYMBOLS = ["StringBundle"];
+const EXPORTED_SYMBOLS = ["StringBundle"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 /**
  * A string bundle.
  *
  * This object presents two APIs: a deprecated one that is equivalent to the API
  * for the stringbundle XBL binding, to make it easy to switch from that binding
  * to this module, and a new one that is simpler and easier to use.
  *
new file mode 100644
--- /dev/null
+++ b/services/common/tests/Makefile.in
@@ -0,0 +1,16 @@
+# 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/.
+
+DEPTH          = ../../..
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = services/common/tests
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = test_services_common
+XPCSHELL_TESTS = unit
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_global.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Where to bind test HTTP servers to.
+const TEST_SERVER_URL = "http://localhost:8080/";
+
+// This has the side-effect of populating Cc, Ci, Cu, Cr. It's best not to
+// ask questions and just accept it.
+do_load_httpd_js();
+const Cm = Components.manager;
+
+let gSyncProfile = do_get_profile();
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let XULAppInfo = {
+  vendor: "Mozilla",
+  name: "XPCShell",
+  ID: "xpcshell@tests.mozilla.org",
+  version: "1",
+  appBuildID: "20100621",
+  platformVersion: "",
+  platformBuildID: "20100621",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
+  invalidateCachesOnRestart: function invalidateCachesOnRestart() { }
+};
+
+let XULAppInfoFactory = {
+  createInstance: function (outer, iid) {
+    if (outer != null)
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    return XULAppInfo.QueryInterface(iid);
+  }
+};
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
+                          "XULAppInfo", "@mozilla.org/xre/app-info;1",
+                          XULAppInfoFactory);
+
+function addResourceAlias() {
+  Cu.import("resource://gre/modules/Services.jsm");
+  const handler = Services.io.getProtocolHandler("resource")
+                  .QueryInterface(Ci.nsIResProtocolHandler);
+
+  let uri = Services.io.newURI("resource:///modules/services-common/", null,
+                               null);
+  handler.setSubstitution("services-common", uri);
+}
+addResourceAlias();
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_helpers.js
@@ -0,0 +1,173 @@
+/* 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/. */
+
+Cu.import("resource://services-common/log4moz.js");
+
+function do_check_empty(obj) {
+  do_check_attribute_count(obj, 0);
+}
+
+function do_check_attribute_count(obj, c) {
+  do_check_eq(c, Object.keys(obj).length);
+}
+
+function do_check_throws(aFunc, aResult, aStack) {
+  if (!aStack) {
+    try {
+      // We might not have a 'Components' object.
+      aStack = Components.stack.caller;
+    } catch (e) {}
+  }
+
+  try {
+    aFunc();
+  } catch (e) {
+    do_check_eq(e.result, aResult, aStack);
+    return;
+  }
+  do_throw("Expected result " + aResult + ", none thrown.", aStack);
+}
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ *        Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
+
+function initTestLogging(level) {
+  function LogStats() {
+    this.errorsLogged = 0;
+  }
+  LogStats.prototype = {
+    format: function BF_format(message) {
+      if (message.level == Log4Moz.Level.Error)
+        this.errorsLogged += 1;
+      return message.loggerName + "\t" + message.levelDesc + "\t" +
+        message.message + "\n";
+    }
+  };
+  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
+
+  var log = Log4Moz.repository.rootLogger;
+  var logStats = new LogStats();
+  var appender = new Log4Moz.DumpAppender(logStats);
+
+  if (typeof(level) == "undefined")
+    level = "Debug";
+  getTestLogger().level = Log4Moz.Level[level];
+
+  log.level = Log4Moz.Level.Trace;
+  appender.level = Log4Moz.Level.Trace;
+  // Overwrite any other appenders (e.g. from previous incarnations)
+  log.ownAppenders = [appender];
+  log.updateAppenders();
+
+  return logStats;
+}
+
+function getTestLogger(component) {
+  return Log4Moz.repository.getLogger("Testing");
+}
+
+function httpd_setup (handlers, port) {
+  let port   = port || 8080;
+  let server = new nsHttpServer();
+  for (let path in handlers) {
+    server.registerPathHandler(path, handlers[path]);
+  }
+  try {
+    server.start(port);
+  } catch (ex) {
+    _("==========================================");
+    _("Got exception starting HTTP server on port " + port);
+    _("Error: " + Utils.exceptionStr(ex));
+    _("Is there a process already listening on port " + port + "?");
+    _("==========================================");
+    do_throw(ex);
+  }
+
+  return server;
+}
+
+function httpd_handler(statusCode, status, body) {
+  return function handler(request, response) {
+    _("Processing request");
+    // Allow test functions to inspect the request.
+    request.body = readBytesFromInputStream(request.bodyInputStream);
+    handler.request = request;
+
+    response.setStatusLine(request.httpVersion, statusCode, status);
+    if (body) {
+      response.bodyOutputStream.write(body, body.length);
+    }
+  };
+}
+
+/*
+ * Read bytes string from an nsIInputStream.  If 'count' is omitted,
+ * all available input is read.
+ */
+function readBytesFromInputStream(inputStream, count) {
+  var BinaryInputStream = Components.Constructor(
+      "@mozilla.org/binaryinputstream;1",
+      "nsIBinaryInputStream",
+      "setInputStream");
+  if (!count) {
+    count = inputStream.available();
+  }
+  return new BinaryInputStream(inputStream).readBytes(count);
+}
+
+/**
+ * Proxy auth helpers.
+ */
+
+/**
+ * Fake a PAC to prompt a channel replacement.
+ */
+let PACSystemSettings = {
+  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
+  contractID: "@mozilla.org/system-proxy-settings;1",
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
+                                         Ci.nsISystemProxySettings]),
+
+  createInstance: function createInstance(outer, iid) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iid);
+  },
+
+  lockFactory: function lockFactory(lock) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  // Replace this URI for each test to avoid caching. We want to ensure that
+  // each test gets a completely fresh setup.
+  PACURI: null,
+  getProxyForURI: function getProxyForURI(aURI) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+};
+
+function installFakePAC() {
+  _("Installing fake PAC.");
+  Cm.nsIComponentRegistrar
+    .registerFactory(PACSystemSettings.CID,
+                     "Fake system proxy-settings",
+                     PACSystemSettings.contractID,
+                     PACSystemSettings);
+}
+
+function uninstallFakePAC() {
+  _("Uninstalling fake PAC.");
+  let CID = PACSystemSettings.CID;
+  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
+}
rename from services/sync/tests/unit/test_async_chain.js
rename to services/common/tests/unit/test_async_chain.js
--- a/services/sync/tests/unit/test_async_chain.js
+++ b/services/common/tests/unit/test_async_chain.js
@@ -1,9 +1,12 @@
-Cu.import("resource://services-sync/async.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/async.js");
 
 function run_test() {
   _("Chain a few async methods, making sure the 'this' object is correct.");
 
   let methods = {
     save: function(x, callback) {
       this.x = x;
       callback(x);
rename from services/sync/tests/unit/test_async_querySpinningly.js
rename to services/common/tests/unit/test_async_querySpinningly.js
--- a/services/sync/tests/unit/test_async_querySpinningly.js
+++ b/services/common/tests/unit/test_async_querySpinningly.js
@@ -1,26 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+
 _("Make sure querySpinningly will synchronously fetch rows for a query asyncly");
-Cu.import("resource://services-sync/async.js");
 
 const SQLITE_CONSTRAINT_VIOLATION = 19;  // http://www.sqlite.org/c3ref/c_abort.html
 
+let Svc = {};
+XPCOMUtils.defineLazyServiceGetter(Svc, "Form",
+                                   "@mozilla.org/satchel/form-history;1",
+                                   "nsIFormHistory2");
+
 function querySpinningly(query, names) {
   let q = Svc.Form.DBConnection.createStatement(query);
   let r = Async.querySpinningly(q, names);
-  q.finalize();    
+  q.finalize();
   return r;
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   _("Make sure the call is async and allows other events to process");
   let isAsync = false;
-  Utils.nextTick(function() { isAsync = true; });
+  CommonUtils.nextTick(function() { isAsync = true; });
   do_check_false(isAsync);
 
   _("Empty out the formhistory table");
   let r0 = querySpinningly("DELETE FROM moz_formhistory");
   do_check_eq(r0, null);
 
   _("Make sure there's nothing there");
   let r1 = querySpinningly("SELECT 1 FROM moz_formhistory");
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_load_modules.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const modules = [
+  "async.js",
+  "log4moz.js",
+  "preferences.js",
+  "rest.js",
+  "stringbundle.js",
+  "utils.js",
+];
+
+function run_test() {
+  for each (let m in modules) {
+    let resource = "resource://services-common/" + m;
+    Components.utils.import(resource, {});
+  }
+}
rename from services/sync/tests/unit/test_log4moz.js
rename to services/common/tests/unit/test_log4moz.js
--- a/services/sync/tests/unit/test_log4moz.js
+++ b/services/common/tests/unit/test_log4moz.js
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Components.utils.import("resource://services-sync/log4moz.js");
-Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+Cu.import("resource://services-common/log4moz.js");
 
 let testFormatter = {
   format: function format(message) {
     return message.loggerName + "\t" + message.levelDesc + "\t" +
            message.message + "\n";
   }
 };
 
rename from services/sync/tests/unit/test_Observers.js
rename to services/common/tests/unit/test_observers.js
--- a/services/sync/tests/unit/test_Observers.js
+++ b/services/common/tests/unit/test_observers.js
@@ -1,13 +1,20 @@
-Components.utils.import("resource://services-sync/ext/Observers.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://services-common/observers.js");
 
 let gSubject = {};
 
-function test_function_observer() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_function_observer() {
   let foo = false;
 
   let onFoo = function(subject, data) {
     foo = !foo;
     do_check_eq(subject, gSubject);
     do_check_eq(data, "some data");
   };
 
@@ -17,19 +24,21 @@ function test_function_observer() {
   // The observer was notified after being added.
   do_check_true(foo);
 
   Observers.remove("foo", onFoo);
   Observers.notify("foo");
 
   // The observer was not notified after being removed.
   do_check_true(foo);
-}
 
-function test_method_observer() {
+  run_next_test();
+});
+
+add_test(function test_method_observer() {
   let obj = {
     foo: false,
     onFoo: function(subject, data) {
       this.foo = !this.foo;
       do_check_eq(subject, gSubject);
       do_check_eq(data, "some data");
     }
   };
@@ -38,19 +47,21 @@ function test_method_observer() {
   Observers.add("foo", obj.onFoo, obj);
   Observers.notify("foo", gSubject, "some data");
   do_check_true(obj.foo);
 
   // The observer is not notified after being removed.
   Observers.remove("foo", obj.onFoo, obj);
   Observers.notify("foo");
   do_check_true(obj.foo);
-}
 
-function test_object_observer() {
+  run_next_test();
+});
+
+add_test(function test_object_observer() {
   let obj = {
     foo: false,
     observe: function(subject, topic, data) {
       this.foo = !this.foo;
 
       do_check_eq(subject, gSubject);
       do_check_eq(topic, "foo");
       do_check_eq(data, "some data");
@@ -63,15 +74,11 @@ function test_object_observer() {
   // The observer is notified after being added.
   do_check_true(obj.foo);
 
   Observers.remove("foo", obj);
   Observers.notify("foo");
 
   // The observer is not notified after being removed.
   do_check_true(obj.foo);
-}
 
-function run_test() {
-  test_function_observer();
-  test_method_observer();
-  test_object_observer();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_Preferences.js
rename to services/common/tests/unit/test_preferences.js
--- a/services/sync/tests/unit/test_Preferences.js
+++ b/services/common/tests/unit/test_preferences.js
@@ -1,119 +1,144 @@
-Components.utils.import("resource://services-sync/ext/Preferences.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/preferences.js");
 
-function test_set_get_pref() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_set_get_pref() {
   Preferences.set("test_set_get_pref.integer", 1);
   do_check_eq(Preferences.get("test_set_get_pref.integer"), 1);
 
   Preferences.set("test_set_get_pref.string", "foo");
   do_check_eq(Preferences.get("test_set_get_pref.string"), "foo");
 
   Preferences.set("test_set_get_pref.boolean", true);
   do_check_eq(Preferences.get("test_set_get_pref.boolean"), true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_pref.");
-}
 
-function test_set_get_branch_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_branch_pref() {
   let prefs = new Preferences("test_set_get_branch_pref.");
 
   prefs.set("something", 1);
   do_check_eq(prefs.get("something"), 1);
   do_check_false(Preferences.has("something"));
 
   // Clean up.
   prefs.reset("something");
-}
 
-function test_set_get_multiple_prefs() {
+  run_next_test();
+});
+
+add_test(function test_set_get_multiple_prefs() {
   Preferences.set({ "test_set_get_multiple_prefs.integer":  1,
                     "test_set_get_multiple_prefs.string":   "foo",
                     "test_set_get_multiple_prefs.boolean":  true });
 
   let [i, s, b] = Preferences.get(["test_set_get_multiple_prefs.integer",
                                    "test_set_get_multiple_prefs.string",
                                    "test_set_get_multiple_prefs.boolean"]);
 
   do_check_eq(i, 1);
   do_check_eq(s, "foo");
   do_check_eq(b, true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_multiple_prefs.");
-}
 
-function test_get_multiple_prefs_with_default_value() {
+  run_next_test();
+});
+
+add_test(function test_get_multiple_prefs_with_default_value() {
   Preferences.set({ "test_get_multiple_prefs_with_default_value.a":  1,
                     "test_get_multiple_prefs_with_default_value.b":  2 });
 
   let [a, b, c] = Preferences.get(["test_get_multiple_prefs_with_default_value.a",
                                    "test_get_multiple_prefs_with_default_value.b",
                                    "test_get_multiple_prefs_with_default_value.c"],
                                   0);
 
   do_check_eq(a, 1);
   do_check_eq(b, 2);
   do_check_eq(c, 0);
 
   // Clean up.
   Preferences.resetBranch("test_get_multiple_prefs_with_default_value.");
-}
 
-function test_set_get_unicode_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_unicode_pref() {
   Preferences.set("test_set_get_unicode_pref", String.fromCharCode(960));
   do_check_eq(Preferences.get("test_set_get_unicode_pref"), String.fromCharCode(960));
 
   // Clean up.
   Preferences.reset("test_set_get_unicode_pref");
-}
 
-function test_set_null_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_null_pref() {
   try {
     Preferences.set("test_set_null_pref", null);
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_undefined_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_undefined_pref() {
   try {
     Preferences.set("test_set_undefined_pref");
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_unsupported_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_unsupported_pref() {
   try {
     Preferences.set("test_set_unsupported_pref", new Array());
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
+
+  run_next_test();
+});
 
 // Make sure that we can get a string pref that we didn't set ourselves
 // (i.e. that the way we get a string pref using getComplexValue doesn't
 // hork us getting a string pref that wasn't set using setComplexValue).
-function test_get_string_pref() {
+add_test(function test_get_string_pref() {
   let svc = Cc["@mozilla.org/preferences-service;1"].
             getService(Ci.nsIPrefService).
             getBranch("");
   svc.setCharPref("test_get_string_pref", "a normal string");
   do_check_eq(Preferences.get("test_get_string_pref"), "a normal string");
 
   // Clean up.
   Preferences.reset("test_get_string_pref");
-}
 
-function test_set_get_number_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_number_pref() {
   Preferences.set("test_set_get_number_pref", 5);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 5);
 
   // Non-integer values get converted to integers.
   Preferences.set("test_set_get_number_pref", 3.14159);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 3);
 
   // Values outside the range -(2^31-1) to 2^31-1 overflow.
@@ -121,61 +146,73 @@ function test_set_get_number_pref() {
     Preferences.set("test_set_get_number_pref", Math.pow(2, 31));
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
 
   // Clean up.
   Preferences.reset("test_set_get_number_pref");
-}
 
-function test_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref() {
   Preferences.set("test_reset_pref", 1);
   Preferences.reset("test_reset_pref");
   do_check_eq(Preferences.get("test_reset_pref"), undefined);
-}
 
-function test_reset_pref_branch() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref_branch() {
   Preferences.set("test_reset_pref_branch.foo", 1);
   Preferences.set("test_reset_pref_branch.bar", 2);
   Preferences.resetBranch("test_reset_pref_branch.");
   do_check_eq(Preferences.get("test_reset_pref_branch.foo"), undefined);
   do_check_eq(Preferences.get("test_reset_pref_branch.bar"), undefined);
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref.
-function test_reset_nonexistent_pref() {
+add_test(function test_reset_nonexistent_pref() {
   Preferences.reset("test_reset_nonexistent_pref");
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref branch.
-function test_reset_nonexistent_pref_branch() {
+add_test(function test_reset_nonexistent_pref_branch() {
   Preferences.resetBranch("test_reset_nonexistent_pref_branch.");
-}
 
-function test_observe_prefs_function() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_function() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something");
   do_check_true(observed);
 
   Preferences.ignore("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something else");
   do_check_true(observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_function");
-}
 
-function test_observe_prefs_object() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_object() {
   let observer = {
     observed: false,
     observe: function() {
       this.observed = !this.observed;
     }
   };
 
   Preferences.observe("test_observe_prefs_object", observer.observe, observer);
@@ -183,19 +220,21 @@ function test_observe_prefs_object() {
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_object", observer.observe, observer);
   Preferences.set("test_observe_prefs_object", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_object");
-}
 
-function test_observe_prefs_nsIObserver() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_nsIObserver() {
   let observer = {
     observed: false,
     observe: function(subject, topic, data) {
       this.observed = !this.observed;
       do_check_true(subject instanceof Ci.nsIPrefBranch);
       do_check_eq(topic, "nsPref:changed");
       do_check_eq(data, "test_observe_prefs_nsIObserver");
     }
@@ -206,84 +245,99 @@ function test_observe_prefs_nsIObserver(
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_nsIObserver", observer);
   Preferences.set("test_observe_prefs_nsIObserver", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_nsIObserver");
-}
 
-function test_observe_exact_pref() {
+  run_next_test();
+});
+
+/*
+add_test(function test_observe_exact_pref() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_exact_pref", observer);
   Preferences.set("test_observe_exact_pref.sub-pref", "something");
   do_check_false(observed);
 
   // Clean up.
   Preferences.ignore("test_observe_exact_pref", observer);
   Preferences.reset("test_observe_exact_pref.sub-pref");
-}
 
-function test_observe_value_of_set_pref() {
+  run_next_test();
+});
+*/
+
+add_test(function test_observe_value_of_set_pref() {
   let observer = function(newVal) { do_check_eq(newVal, "something") };
 
   Preferences.observe("test_observe_value_of_set_pref", observer);
   Preferences.set("test_observe_value_of_set_pref", "something");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_set_pref", observer);
   Preferences.reset("test_observe_value_of_set_pref");
-}
 
-function test_observe_value_of_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_observe_value_of_reset_pref() {
   let observer = function(newVal) { do_check_true(typeof newVal == "undefined") };
 
   Preferences.set("test_observe_value_of_reset_pref", "something");
   Preferences.observe("test_observe_value_of_reset_pref", observer);
   Preferences.reset("test_observe_value_of_reset_pref");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_reset_pref", observer);
-}
 
-function test_has_pref() {
+  run_next_test();
+});
+
+add_test(function test_has_pref() {
   do_check_false(Preferences.has("test_has_pref"));
   Preferences.set("test_has_pref", "foo");
   do_check_true(Preferences.has("test_has_pref"));
 
   Preferences.set("test_has_pref.foo", "foo");
   Preferences.set("test_has_pref.bar", "bar");
   let [hasFoo, hasBar, hasBaz] = Preferences.has(["test_has_pref.foo",
                                                   "test_has_pref.bar",
                                                   "test_has_pref.baz"]);
   do_check_true(hasFoo);
   do_check_true(hasBar);
   do_check_false(hasBaz);
 
   // Clean up.
   Preferences.resetBranch("test_has_pref");
-}
 
-function test_isSet_pref() {
+  run_next_test();
+});
+
+add_test(function test_isSet_pref() {
   // Use a pref that we know has a default value but no user-set value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.isSet("toolkit.defaultChromeURI"));
   Preferences.set("toolkit.defaultChromeURI", "foo");
   do_check_true(Preferences.isSet("toolkit.defaultChromeURI"));
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_lock_prefs() {
+  run_next_test();
+});
+
+/*
+add_test(function test_lock_prefs() {
   // Use a pref that we know has a default value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_true(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
@@ -293,19 +347,22 @@ function test_lock_prefs() {
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), val);
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_site_prefs() {
+  run_next_test();
+});
+*/
+
+add_test(function test_site_prefs() {
   let prefs = Preferences.site("www.example.com");
 
   prefs.set("test_site_prefs.integer", 1);
   do_check_eq(prefs.get("test_site_prefs.integer"), 1);
   do_check_true(prefs.has("test_site_prefs.integer"));
   do_check_false(Preferences.has("test_site_prefs.integer"));
   prefs.reset("test_site_prefs.integer");
   do_check_false(prefs.has("test_site_prefs.integer"));
@@ -318,37 +375,11 @@ function test_site_prefs() {
   do_check_false(prefs.has("test_site_prefs.string"));
 
   prefs.set("test_site_prefs.boolean", true);
   do_check_eq(prefs.get("test_site_prefs.boolean"), true);
   do_check_true(prefs.has("test_site_prefs.boolean"));
   do_check_false(Preferences.has("test_site_prefs.boolean"));
   prefs.reset("test_site_prefs.boolean");
   do_check_false(prefs.has("test_site_prefs.boolean"));
-}
 
-
-function run_test() {
-  test_set_get_pref();
-  test_set_get_branch_pref();
-  test_set_get_multiple_prefs();
-  test_get_multiple_prefs_with_default_value();
-  test_set_get_unicode_pref();
-  test_set_null_pref();
-  test_set_undefined_pref();
-  test_set_unsupported_pref();
-  test_get_string_pref();
-  test_set_get_number_pref();
-  test_reset_pref();
-  test_reset_pref_branch();
-  test_reset_nonexistent_pref();
-  test_reset_nonexistent_pref_branch();
-  test_observe_prefs_function();
-  test_observe_prefs_object();
-  test_observe_prefs_nsIObserver();
-  //test_observe_exact_pref();
-  test_observe_value_of_set_pref();
-  test_observe_value_of_reset_pref();
-  test_has_pref();
-  test_isSet_pref();
-  //test_lock_prefs();
-  test_site_prefs();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_restrequest.js
rename to services/common/tests/unit/test_restrequest.js
--- a/services/sync/tests/unit/test_restrequest.js
+++ b/services/common/tests/unit/test_restrequest.js
@@ -1,19 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/rest.js");
-Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-common/utils.js");
 
 const TEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
+//DEBUG = true;
+
 function run_test() {
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Services.Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
 }
 
 /**
  * Initializing a RESTRequest with an invalid URI throws
  * NS_ERROR_MALFORMED_URI.
@@ -135,17 +139,17 @@ add_test(function test_get() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Huzzah!");
     do_check_eq(handler.request.method, "GET");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.get(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -184,17 +188,17 @@ add_test(function test_put() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "PUT");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.put("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -233,17 +237,17 @@ add_test(function test_post() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "POST");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.post("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -279,17 +283,17 @@ add_test(function test_delete() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
     do_check_eq(handler.request.method, "DELETE");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.delete(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -462,17 +466,17 @@ add_test(function test_get_no_headers() 
 /**
  * Test changing the URI after having created the request.
  */
 add_test(function test_changing_uri() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest("http://localhost:8080/the-wrong-resource");
-  request.uri = Utils.makeURI(TEST_RESOURCE_URL);
+  request.uri = CommonUtils.makeURI(TEST_RESOURCE_URL);
   request.get(function (error) {
     do_check_eq(error, null);
     do_check_eq(this.response.status, 200);
     server.stop(run_next_test);
   });
 });
 
 /**
@@ -567,17 +571,17 @@ add_test(function test_abort() {
   request.abort();
 
   // Aborting an already aborted request is pointless and will throw.
   do_check_throws(function () {
     request.abort();
   });
 
   do_check_eq(request.status, request.ABORTED);
-  Utils.nextTick(function () {
+  CommonUtils.nextTick(function () {
     server.stop(run_next_test);
   });
 });
 
 /**
  * A non-zero 'timeout' property specifies the amount of seconds to wait after
  * channel activity until the request is automatically canceled.
  */
rename from services/sync/tests/unit/test_utils_makeURI.js
rename to services/common/tests/unit/test_utils_makeURI.js
--- a/services/sync/tests/unit/test_utils_makeURI.js
+++ b/services/common/tests/unit/test_utils_makeURI.js
@@ -1,66 +1,66 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Make sure uri strings are converted to nsIURIs");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _test_makeURI();
 }
 
 function _test_makeURI() {
   _("Check http uris");
   let uri1 = "http://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri1).spec, uri1);
+  do_check_eq(CommonUtils.makeURI(uri1).spec, uri1);
   let uri2 = "http://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri2).spec, uri2);
+  do_check_eq(CommonUtils.makeURI(uri2).spec, uri2);
   let uri3 = "http://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uri3).spec, uri3);
+  do_check_eq(CommonUtils.makeURI(uri3).spec, uri3);
   let uri4 = "http://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uri4).spec, uri4);
+  do_check_eq(CommonUtils.makeURI(uri4).spec, uri4);
   let uri5 = "http://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uri5).spec, uri5);
+  do_check_eq(CommonUtils.makeURI(uri5).spec, uri5);
   let uri6 = "http://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uri6).spec, uri6);
+  do_check_eq(CommonUtils.makeURI(uri6).spec, uri6);
 
   _("Check https uris");
   let uris1 = "https://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris1).spec, uris1);
+  do_check_eq(CommonUtils.makeURI(uris1).spec, uris1);
   let uris2 = "https://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris2).spec, uris2);
+  do_check_eq(CommonUtils.makeURI(uris2).spec, uris2);
   let uris3 = "https://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uris3).spec, uris3);
+  do_check_eq(CommonUtils.makeURI(uris3).spec, uris3);
   let uris4 = "https://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uris4).spec, uris4);
+  do_check_eq(CommonUtils.makeURI(uris4).spec, uris4);
   let uris5 = "https://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uris5).spec, uris5);
+  do_check_eq(CommonUtils.makeURI(uris5).spec, uris5);
   let uris6 = "https://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uris6).spec, uris6);
+  do_check_eq(CommonUtils.makeURI(uris6).spec, uris6);
 
   _("Check chrome uris");
   let uric1 = "chrome://browser/content/browser.xul";
-  do_check_eq(Utils.makeURI(uric1).spec, uric1);
+  do_check_eq(CommonUtils.makeURI(uric1).spec, uric1);
   let uric2 = "chrome://browser/skin/browser.css";
-  do_check_eq(Utils.makeURI(uric2).spec, uric2);
+  do_check_eq(CommonUtils.makeURI(uric2).spec, uric2);
   let uric3 = "chrome://browser/locale/browser.dtd";
-  do_check_eq(Utils.makeURI(uric3).spec, uric3);
+  do_check_eq(CommonUtils.makeURI(uric3).spec, uric3);
 
   _("Check about uris");
   let uria1 = "about:weave";
-  do_check_eq(Utils.makeURI(uria1).spec, uria1);
+  do_check_eq(CommonUtils.makeURI(uria1).spec, uria1);
   let uria2 = "about:weave/";
-  do_check_eq(Utils.makeURI(uria2).spec, uria2);
+  do_check_eq(CommonUtils.makeURI(uria2).spec, uria2);
   let uria3 = "about:weave/path";
-  do_check_eq(Utils.makeURI(uria3).spec, uria3);
+  do_check_eq(CommonUtils.makeURI(uria3).spec, uria3);
   let uria4 = "about:weave/multi/path";
-  do_check_eq(Utils.makeURI(uria4).spec, uria4);
+  do_check_eq(CommonUtils.makeURI(uria4).spec, uria4);
   let uria5 = "about:weave/?query";
-  do_check_eq(Utils.makeURI(uria5).spec, uria5);
+  do_check_eq(CommonUtils.makeURI(uria5).spec, uria5);
   let uria6 = "about:weave/#hash";
-  do_check_eq(Utils.makeURI(uria6).spec, uria6);
+  do_check_eq(CommonUtils.makeURI(uria6).spec, uria6);
 
   _("Invalid uris are undefined");
-  do_check_eq(Utils.makeURI("mozillalabs.com"), undefined);
-  do_check_eq(Utils.makeURI("chrome://badstuff"), undefined);
-  do_check_eq(Utils.makeURI("this is a test"), undefined);
+  do_check_eq(CommonUtils.makeURI("mozillalabs.com"), undefined);
+  do_check_eq(CommonUtils.makeURI("chrome://badstuff"), undefined);
+  do_check_eq(CommonUtils.makeURI("this is a test"), undefined);
 }
rename from services/sync/tests/unit/test_utils_namedTimer.js
rename to services/common/tests/unit/test_utils_namedTimer.js
--- a/services/sync/tests/unit/test_utils_namedTimer.js
+++ b/services/common/tests/unit/test_utils_namedTimer.js
@@ -1,69 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_required_args() {
   try {
-    Utils.namedTimer(function callback() {
+    CommonUtils.namedTimer(function callback() {
       do_throw("Shouldn't fire.");
     }, 0);
     do_throw("Should have thrown!");
   } catch(ex) {
     run_next_test();
   }
 });
 
 add_test(function test_simple() {
-  _("Test basic properties of Utils.namedTimer.");
+  _("Test basic properties of CommonUtils.namedTimer.");
 
   const delay = 200;
   let that = {};
   let t0 = Date.now();
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_check_eq(this, that);
     do_check_eq(this._zetimer, null);
     do_check_true(timer instanceof Ci.nsITimer);
     // Difference should be ~delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true(Date.now() > t0);
     run_next_test();
   }, delay, that, "_zetimer");
 });
 
 add_test(function test_delay() {
   _("Test delaying a timer that hasn't fired yet.");
-  
+
   const delay = 100;
   let that = {};
   let t0 = Date.now();
   function callback(timer) {
     // Difference should be ~2*delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true((Date.now() - t0) > delay);
     run_next_test();
   }
-  Utils.namedTimer(callback, delay, that, "_zetimer");
-  Utils.namedTimer(callback, 2 * delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, 2 * delay, that, "_zetimer");
   run_next_test();
 });
 
 add_test(function test_clear() {
   _("Test clearing a timer that hasn't fired yet.");
 
   const delay = 0;
   let that = {};
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_throw("Shouldn't fire!");
   }, delay, that, "_zetimer");
 
   that._zetimer.clear();
   do_check_eq(that._zetimer, null);
-  Utils.nextTick(run_next_test);
+  CommonUtils.nextTick(run_next_test);
 
   run_next_test();
 });
rename from services/sync/tests/unit/test_utils_stackTrace.js
rename to services/common/tests/unit/test_utils_stackTrace.js
--- a/services/sync/tests/unit/test_utils_stackTrace.js
+++ b/services/common/tests/unit/test_utils_stackTrace.js
@@ -1,30 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Define some functions in well defined line positions for the test");
 function foo(v) bar(v + 1); // line 2
 function bar(v) baz(v + 1); // line 3
 function baz(v) { throw new Error(v + 1); } // line 4
 
 _("Make sure lazy constructor calling/assignment works");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _("Make sure functions, arguments, files are pretty printed in the trace");
   let trace = "";
   try {
     foo(0);
   }
   catch(ex) {
-    trace = Utils.stackTrace(ex);
+    trace = CommonUtils.stackTrace(ex);
   }
   _("Got trace:", trace);
   do_check_neq(trace, "");
 
-  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:4");
-  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:3");
-  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:2");
+  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:7");
+  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:6");
+  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:5");
   _("String positions:", bazPos, barPos, fooPos);
 
   _("Make sure the desired messages show up");
   do_check_true(bazPos >= 0);
   do_check_true(barPos > bazPos);
   do_check_true(fooPos > barPos);
 }
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/xpcshell.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+head = head_global.js head_helpers.js
+tail =
+
+# Test load modules first so syntax failures are caught early.
+[test_load_modules.js]
+
+[test_utils_makeURI.js]
+[test_utils_namedTimer.js]
+[test_utils_stackTrace.js]
+
+[test_async_chain.js]
+[test_async_querySpinningly.js]
+[test_log4moz.js]
+[test_observers.js]
+[test_preferences.js]
+[test_restrequest.js]
new file mode 100644
--- /dev/null
+++ b/services/common/utils.js
@@ -0,0 +1,125 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["CommonUtils"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/log4moz.js");
+
+let CommonUtils = {
+  exceptionStr: function exceptionStr(e) {
+    let message = e.message ? e.message : e;
+    return message + " " + CommonUtils.stackTrace(e);
+  },
+
+  stackTrace: function stackTrace(e) {
+    // Wrapped nsIException
+    if (e.location) {
+      let frame = e.location;
+      let output = [];
+      while (frame) {
+        // Works on frames or exceptions, munges file:// URIs to shorten the paths
+        // FIXME: filename munging is sort of hackish, might be confusing if
+        // there are multiple extensions with similar filenames
+        let str = "<file:unknown>";
+
+        let file = frame.filename || frame.fileName;
+        if (file){
+          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
+        }
+
+        if (frame.lineNumber){
+          str += ":" + frame.lineNumber;
+        }
+        if (frame.name){
+          str = frame.name + "()@" + str;
+        }
+
+        if (str){
+          output.push(str);
+        }
+        frame = frame.caller;
+      }
+      return "Stack trace: " + output.join(" < ");
+    }
+    // Standard JS exception
+    if (e.stack){
+      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
+        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
+    }
+
+    return "No traceback available";
+  },
+
+  /**
+   * Create a nsIURI instance from a string.
+   */
+  makeURI: function makeURI(URIString) {
+    if (!URIString)
+      return null;
+    try {
+      return Services.io.newURI(URIString, null, null);
+    } catch (e) {
+      let log = Log4Moz.repository.getLogger("Common.Utils");
+      log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
+      return null;
+    }
+  },
+
+  /**
+   * Execute a function on the next event loop tick.
+   *
+   * @param callback
+   *        Function to invoke.
+   * @param thisObj [optional]
+   *        Object to bind the callback to.
+   */
+  nextTick: function nextTick(callback, thisObj) {
+    if (thisObj) {
+      callback = callback.bind(thisObj);
+    }
+    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Return a timer that is scheduled to call the callback after waiting the
+   * provided time or as soon as possible. The timer will be set as a property
+   * of the provided object with the given timer name.
+   */
+  namedTimer: function namedTimer(callback, wait, thisObj, name) {
+    if (!thisObj || !name) {
+      throw "You must provide both an object and a property name for the timer!";
+    }
+
+    // Delay an existing timer if it exists
+    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
+      thisObj[name].delay = wait;
+      return;
+    }
+
+    // Create a special timer that we can add extra properties
+    let timer = {};
+    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+    // Provide an easy way to clear out the timer
+    timer.clear = function() {
+      thisObj[name] = null;
+      timer.cancel();
+    };
+
+    // Initialize the timer with a smart callback
+    timer.initWithCallback({
+      notify: function notify() {
+        // Clear out the timer once it's been triggered
+        timer.clear();
+        callback.call(thisObj, timer);
+      }
+    }, wait, timer.TYPE_ONE_SHOT);
+
+    return thisObj[name] = timer;
+  },
+
+};
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -30,30 +30,24 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
-MAKEFILES_crypto="
+add_makefiles "
+  services/Makefile
+  services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
-"
-
-MAKEFILES_sync="
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
-add_makefiles "
-  services/Makefile
-  $MAKEFILES_crypto
-  $MAKEFILES_sync
-"
-
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
+    services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -1,9 +1,10 @@
 # Weave.js
 component {74b89fb0-f200-4ae8-a3ec-dd164117f6de} Weave.js
 contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
 category app-startup WeaveService service,@mozilla.org/weave/service;1
 component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
 contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
 # Register resource aliases
 resource services-sync resource:///modules/services-sync/
+resource services-common resource:///modules/services-common/
 resource services-crypto resource:///modules/services-crypto/
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -47,17 +47,17 @@
  * standalone file so it could be more easily understood, tested, and
  * hopefully ported.
  */
 
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 
 const DEFAULT_STATE_FILE = "addonsreconciler";
 
 const CHANGE_INSTALLED   = 1;
 const CHANGE_UNINSTALLED = 2;
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -41,22 +41,22 @@
 const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine',
                           'Tracker', 'Store'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 /*
  * Trackers are associated with a single engine and deal with
  * listening for changes to their particular data type.
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -67,18 +67,18 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-sync/addonsreconciler.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/async.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/preferences.js");
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 
 const EXPORTED_SYMBOLS = ["AddonsEngine"];
 
 // 7 days in milliseconds.
 const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -46,17 +46,17 @@ const EXPORTED_SYMBOLS = ['BookmarksEngi
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
 Cu.import("resource://services-sync/main.js");      // For access to Service.
 
 const ALLBOOKMARKS_ANNO    = "AllBookmarks";
 const DESCRIPTION_ANNO     = "bookmarkProperties/description";
 const SIDEBAR_ANNO         = "bookmarkProperties/loadInSidebar";
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -37,19 +37,19 @@
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["Clients", "ClientsRec"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const CLIENTS_TTL = 1814400; // 21 days
 const CLIENTS_TTL_REFRESH = 604800; // 7 days
 
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -38,20 +38,20 @@ const EXPORTED_SYMBOLS = ['FormEngine', 
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const FORMS_TTL = 5184000; // 60 days
 
 function FormRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 FormRec.prototype = {
   __proto__: CryptoWrapper.prototype,
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -46,19 +46,19 @@ const Cu = Components.utils;
 const Cr = Components.results;
 
 const HISTORY_TTL = 5184000; // 60 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function HistoryRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 HistoryRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.History",
   ttl: HISTORY_TTL
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -42,17 +42,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const WEAVE_SYNC_PREFS = "services.sync.prefs.sync.";
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function PrefRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 PrefRec.prototype = {
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -46,17 +46,17 @@ const TABS_TTL = 604800; // 7 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 // It is safer to inspect the private browsing preferences rather than
 // the flags of nsIPrivateBrowsingService.  The user may have turned on
 // "Never remember history" in the same session, or Firefox was started
 // with the -private command line argument.  In both cases, the
 // "autoStarted" flag of nsIPrivateBrowsingService will be wrong.
 const PBPrefs = new Preferences("browser.privatebrowsing.");
 
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const EXPORTED_SYMBOLS = ["Identity", "IdentityManager"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 XPCOMUtils.defineLazyGetter(this, "Identity", function() {
   return new IdentityManager();
 });
 
 /**
  * Manages identity and authentication for Sync.
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -35,18 +35,18 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const EXPORTED_SYMBOLS = ["JPAKEClient"];
 
 const REQUEST_TIMEOUT         = 60; // 1 minute
 const KEYEXCHANGE_VERSION     = 3;
 
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -7,17 +7,17 @@
 const EXPORTED_SYMBOLS = [
   "BulkKeyBundle",
   "SyncKeyBundle"
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /**
  * Represents a pair of keys.
  *
  * Each key stored in a key bundle is 256 bits. One key is used for symmetric
  * encryption. The other is used for HMAC.
  *
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -36,18 +36,18 @@
 
 const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let Notifications = {
   // Match the referenced values in toolkit/content/widgets/notification.xml.
   get PRIORITY_INFO()     1, // PRIORITY_INFO_LOW
   get PRIORITY_WARNING()  4, // PRIORITY_WARNING_LOW
   get PRIORITY_ERROR()    7, // PRIORITY_CRITICAL_LOW
 
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -40,17 +40,17 @@
 
 const EXPORTED_SYMBOLS = ["SyncScheduler",
                           "ErrorHandler",
                           "SendCredentialsController"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/status.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 let SyncScheduler = {
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -46,17 +46,17 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 const CRYPTO_COLLECTION = "crypto";
 const KEYS_WBO = "keys";
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 function WBORecord(collection, id) {
   this.data = {};
   this.payload = {};
   this.collection = collection;      // Optional.
   this.id = id;                      // Optional.
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -42,22 +42,22 @@ const EXPORTED_SYMBOLS = [
   "Resource"
 ];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /*
  * AsyncResource represents a remote network resource, identified by a URI.
  * Create an instance like so:
  * 
  *   let resource = new AsyncResource("http://foobar.com/path/to/resource");
  * 
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -35,592 +35,27 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
 
-const EXPORTED_SYMBOLS = ["RESTRequest", "SyncStorageRequest"];
+const EXPORTED_SYMBOLS = ["SyncStorageRequest"];
 
 const STORAGE_REQUEST_TIMEOUT = 5 * 60; // 5 minutes
 
 /**
- * Single use HTTP requests to RESTish resources.
- * 
- * @param uri
- *        URI for the request. This can be an nsIURI object or a string
- *        that can be used to create one. An exception will be thrown if
- *        the string is not a valid URI.
- *
- * Examples:
- *
- * (1) Quick GET request:
- *
- *   new RESTRequest("http://server/rest/resource").get(function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     if (!this.response.success) {
-
- *  *       // Bail out if we're not getting an HTTP 2xx code.
- *       processHTTPError(this.response.status);
- *       return;
- *     }
- *     processData(this.response.body);
- *   });
- *
- * (2) Quick PUT request (non-string data is automatically JSONified)
- *
- *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
- *     ...
- *   });
- *
- * (3) Streaming GET
- *
- *   let request = new RESTRequest("http://server/rest/resource");
- *   request.setHeader("Accept", "application/newlines");
- *   request.onComplete = function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     callbackAfterRequestHasCompleted()
- *   });
- *   request.onProgress = function () {
- *     if (!this.response.success) {
- *       // Bail out if we're not getting an HTTP 2xx code.
- *       return;
- *     }
- *     // Process body data and reset it so we don't process the same data twice.
- *     processIncrementalData(this.response.body);
- *     this.response.body = "";
- *   });
- *   request.get();
- */
-function RESTRequest(uri) {
-  this.status = this.NOT_SENT;
-
-  // If we don't have an nsIURI object yet, make one. This will throw if
-  // 'uri' isn't a valid URI string.
-  if (!(uri instanceof Ci.nsIURI)) {
-    uri = Services.io.newURI(uri, null, null);
-  }
-  this.uri = uri;
-
-  this._headers = {};
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTRequest.prototype = {
-
-  _logName: "Sync.RESTRequest",
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIBadCertListener2,
-    Ci.nsIInterfaceRequestor,
-    Ci.nsIChannelEventSink
-  ]),
-
-  /*** Public API: ***/
-
-  /**
-   * URI for the request (an nsIURI object).
-   */
-  uri: null,
-
-  /**
-   * HTTP method (e.g. "GET")
-   */
-  method: null,
-
-  /**
-   * RESTResponse object
-   */
-  response: null,
-
-  /**
-   * nsIRequest load flags. Don't do any caching by default.
-   */
-  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
-
-  /**
-   * nsIHttpChannel
-   */
-  channel: null,
-
-  /**
-   * Flag to indicate the status of the request.
-   *
-   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
-   */
-  status: null,
-
-  NOT_SENT:    0,
-  SENT:        1,
-  IN_PROGRESS: 2,
-  COMPLETED:   4,
-  ABORTED:     8,
-
-  /**
-   * Request timeout (in seconds, though decimal values can be used for
-   * up to millisecond granularity.)
-   *
-   * 0 for no timeout.
-   */
-  timeout: null,
-
-  /**
-   * Called when the request has been completed, including failures and
-   * timeouts.
-   * 
-   * @param error
-   *        Error that occurred while making the request, null if there
-   *        was no error.
-   */
-  onComplete: function onComplete(error) {
-  },
-
-  /**
-   * Called whenever data is being received on the channel. If this throws an
-   * exception, the request is aborted and the exception is passed as the
-   * error to onComplete().
-   */
-  onProgress: function onProgress() {
-  },
-
-  /**
-   * Set a request header.
-   */
-  setHeader: function setHeader(name, value) {
-    this._headers[name.toLowerCase()] = value;
-  },
-
-  /**
-   * Perform an HTTP GET.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  get: function get(onComplete, onProgress) {
-    return this.dispatch("GET", null, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP PUT.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  put: function put(data, onComplete, onProgress) {
-    return this.dispatch("PUT", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP POST.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  post: function post(data, onComplete, onProgress) {
-    return this.dispatch("POST", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP DELETE.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  delete: function delete_(onComplete, onProgress) {
-    return this.dispatch("DELETE", null, onComplete, onProgress);
-  },
-
-  /**
-   * Abort an active request.
-   */
-  abort: function abort() {
-    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
-      throw "Can only abort a request that has been sent.";
-    }
-
-    this.status = this.ABORTED;
-    this.channel.cancel(Cr.NS_BINDING_ABORTED);
-
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-  },
-
-  /*** Implementation stuff ***/
-
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
-    if (this.status != this.NOT_SENT) {
-      throw "Request has already been sent!";
-    }
-
-    this.method = method;
-    if (onComplete) {
-      this.onComplete = onComplete;
-    }
-    if (onProgress) {
-      this.onProgress = onProgress;
-    }
-
-    // Create and initialize HTTP channel.
-    let channel = Services.io.newChannelFromURI(this.uri, null, null)
-                          .QueryInterface(Ci.nsIRequest)
-                          .QueryInterface(Ci.nsIHttpChannel);
-    this.channel = channel;
-    channel.loadFlags |= this.loadFlags;
-    channel.notificationCallbacks = this;
-
-    // Set request headers.
-    let headers = this._headers;
-    for (let key in headers) {
-      if (key == 'authorization') {
-        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
-      } else {
-        this._log.trace("HTTP Header " + key + ": " + headers[key]);
-      }
-      channel.setRequestHeader(key, headers[key], false);
-    }
-
-    // Set HTTP request body.
-    if (method == "PUT" || method == "POST") {
-      // Convert non-string bodies into JSON.
-      if (typeof data != "string") {
-        data = JSON.stringify(data);
-      }
-
-      this._log.debug(method + " Length: " + data.length);
-      if (this._log.level <= Log4Moz.Level.Trace) {
-        this._log.trace(method + " Body: " + data);
-      }
-
-      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
-                     .createInstance(Ci.nsIStringInputStream);
-      stream.setData(data, data.length);
-
-      let type = headers["content-type"] || "text/plain";
-      channel.QueryInterface(Ci.nsIUploadChannel);
-      channel.setUploadStream(stream, type, data.length);
-    }
-    // We must set this after setting the upload stream, otherwise it
-    // will always be 'PUT'. Yeah, I know.
-    channel.requestMethod = method;
-
-    // Blast off!
-    channel.asyncOpen(this, null);
-    this.status = this.SENT;
-    this.delayTimeout();
-    return this;
-  },
-
-  /**
-   * Create or push back the abort timer that kills this request.
-   */
-  delayTimeout: function delayTimeout() {
-    if (this.timeout) {
-      Utils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
-                       "timeoutTimer");
-    }
-  },
-
-  /**
-   * Abort the request based on a timeout.
-   */
-  abortTimeout: function abortTimeout() {
-    this.abort();
-    let error = Components.Exception("Aborting due to channel inactivity.",
-                                     Cr.NS_ERROR_NET_TIMEOUT);
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortTimeout.")
-      return;
-    }
-    this.onComplete(error);
-  },
-
-  /*** nsIStreamListener ***/
-
-  onStartRequest: function onStartRequest(channel) {
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
-      this.status = this.ABORTED;
-      channel.cancel(Cr.NS_BINDING_ABORTED);
-      return;
-    }
-
-    this.status = this.IN_PROGRESS;
-
-    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
-                    channel.URI.spec);
-
-    // Create a response object and fill it with some data.
-    let response = this.response = new RESTResponse();
-    response.request = this;
-    response.body = "";
-
-    // Define this here so that we don't have make a new one each time
-    // onDataAvailable() gets called.
-    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
-                          .createInstance(Ci.nsIScriptableInputStream);
-
-    this.delayTimeout();
-  },
-
-  onStopRequest: function onStopRequest(channel, context, statusCode) {
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-
-    // We don't want to do anything for a request that's already been aborted.
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      this.status = this.ABORTED;
-      return;
-    }
-    this.status = this.COMPLETED;
-
-    let statusSuccess = Components.isSuccessCode(statusCode);
-    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
-    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
-                    " returned status code " + statusCode);
-
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortRequest.");
-      this.onProgress = null;
-      return;
-    }
-
-    // Throw the failure code and stop execution.  Use Components.Exception()
-    // instead of Error() so the exception is QI-able and can be passed across
-    // XPCOM borders while preserving the status code.
-    if (!statusSuccess) {
-      let message = Components.Exception("", statusCode).name;
-      let error = Components.Exception(message, statusCode);
-      this.onComplete(error);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this._log.debug(this.method + " " + uri + " " + this.response.status);
-
-    // Additionally give the full response body when Trace logging.
-    if (this._log.level <= Log4Moz.Level.Trace) {
-      this._log.trace(this.method + " body: " + this.response.body);
-    }
-
-    delete this._inputStream;
-
-    this.onComplete(null);
-    this.onComplete = this.onProgress = null;
-  },
-
-  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
-    this._inputStream.init(stream);
-    try {
-      this.response.body += this._inputStream.read(count);
-    } catch (ex) {
-      this._log.warn("Exception thrown reading " + count +
-                     " bytes from the channel.");
-      this._log.debug(Utils.exceptionStr(ex));
-      throw ex;
-    }
-
-    try {
-      this.onProgress();
-    } catch (ex) {
-      this._log.warn("Got exception calling onProgress handler, aborting " +
-                     this.method + " " + req.URI.spec);
-      this._log.debug("Exception: " + Utils.exceptionStr(ex));
-      this.abort();
-
-      if (!this.onComplete) {
-        this._log.error("Unexpected error: onComplete not defined in " +
-                        "onDataAvailable.");
-        this.onProgress = null;
-        return;
-      }
-
-      this.onComplete(ex);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this.delayTimeout();
-  },
-
-  /*** nsIInterfaceRequestor ***/
-
-  getInterface: function(aIID) {
-    return this.QueryInterface(aIID);
-  },
-
-  /*** nsIBadCertListener2 ***/
-
-  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
-    this._log.warn("Invalid HTTPS certificate encountered!");
-    // Suppress invalid HTTPS certificate warnings in the UI.
-    // (The request will still fail.)
-    return true;
-  },
-
-  /*** nsIChannelEventSink ***/
-  asyncOnChannelRedirect:
-    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-
-    try {
-      newChannel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
-      return;
-    }
-
-    this.channel = newChannel;
-
-    // We let all redirects proceed.
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  }
-};
-
-
-/**
- * Response object for a RESTRequest. This will be created automatically by
- * the RESTRequest.
- */
-function RESTResponse() {
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTResponse.prototype = {
-
-  _logName: "Sync.RESTResponse",
-
-  /**
-   * Corresponding REST request
-   */
-  request: null,
-
-  /**
-   * HTTP status code
-   */
-  get status() {
-    let status;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      status = channel.responseStatus;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP status code:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.status;
-    return this.status = status;
-  },
-
-  /**
-   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
-   */
-  get success() {
-    let success;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      success = channel.requestSucceeded;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP success flag:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.success;
-    return this.success = success;
-  },
-
-  /**
-   * Object containing HTTP headers (keyed as lower case)
-   */
-  get headers() {
-    let headers = {};
-    try {
-      this._log.trace("Processing response headers.");
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      channel.visitResponseHeaders(function (header, value) {
-        headers[header.toLowerCase()] = value;
-      });
-    } catch (ex) {
-      this._log.debug("Caught exception processing response headers:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-
-    delete this.headers;
-    return this.headers = headers;
-  },
-
-  /**
-   * HTTP body (string)
-   */
-  body: null
-
-};
-
-
-/**
  * RESTRequest variant for use against a Sync storage server.
  */
 function SyncStorageRequest(uri) {
   RESTRequest.call(this, uri);
 }
 SyncStorageRequest.prototype = {
 
   __proto__: RESTRequest.prototype,
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -58,19 +58,19 @@ const KEYS_WBO = "keys";
 
 const LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -36,17 +36,17 @@
 const EXPORTED_SYMBOLS = ["Status"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let Status = {
   _log: Log4Moz.repository.getLogger("Sync.Status"),
   _authManager: Identity,
   ready: false,
 
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -33,38 +33,44 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils",
                           "FileUtils", "Utils", "Async", "Svc", "Str"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/stringbundle.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 /*
  * Utility functions
  */
 
 let Utils = {
+  // Alias in functions from CommonUtils. These previously were defined here.
+  // In the ideal world, references to these would be removed.
+  nextTick: CommonUtils.nextTick,
+  namedTimer: CommonUtils.namedTimer,
+  exceptionStr: CommonUtils.exceptionStr,
+  stackTrace: CommonUtils.stackTrace,
+  makeURI: CommonUtils.makeURI,
+
   /**
    * Wrap a function to catch all exceptions and log them
    *
    * @usage MyObj._catch = Utils.catch;
    *        MyObj.foo = function() { this._catch(func)(); }
    *        
    * Optionally pass a function which will be called if an
    * exception occurs.
@@ -264,72 +270,28 @@ let Utils = {
     // Do the same for b's keys but skip those that we already checked
     for (let k in b)
       if (!(k in a) && !eq(a[k], b[k]))
         return false;
 
     return true;
   },
 
-  exceptionStr: function Weave_exceptionStr(e) {
-    let message = e.message ? e.message : e;
-    return message + " " + Utils.stackTrace(e);
-  },
-  
-  stackTrace: function Weave_stackTrace(e) {
-    // Wrapped nsIException
-    if (e.location){
-      let frame = e.location; 
-      let output = [];
-      while (frame) {
-      	// Works on frames or exceptions, munges file:// URIs to shorten the paths
-        // FIXME: filename munging is sort of hackish, might be confusing if
-        // there are multiple extensions with similar filenames
-        let str = "<file:unknown>";
-
-        let file = frame.filename || frame.fileName;
-        if (file){
-          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
-        }
-
-        if (frame.lineNumber){
-          str += ":" + frame.lineNumber;
-        }
-        if (frame.name){
-          str = frame.name + "()@" + str;
-        }
-
-        if (str){
-          output.push(str);
-        }
-        frame = frame.caller;
-      }
-      return "Stack trace: " + output.join(" < ");
-    }
-    // Standard JS exception
-    if (e.stack){
-      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
-        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
-    }
-
-    return "No traceback available";
-  },
-  
   // Generator and discriminator for HMAC exceptions.
-  // Split these out in case we want to make them richer in future, and to 
+  // Split these out in case we want to make them richer in future, and to
   // avoid inevitable confusion if the message changes.
   throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
     throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
   },
-  
+
   isHMACMismatch: function isHMACMismatch(ex) {
     const hmacFail = "Record SHA256 HMAC mismatch: ";
     return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
   },
-  
+
   /**
    * UTF8-encode a message and hash it with the given hasher. Returns a
    * string containing bytes. The hasher is reset if it's an HMAC hasher.
    */
   digestUTF8: function digestUTF8(message, hasher) {
     let data = this._utf8Converter.convertToByteArray(message, {});
     hasher.update(data, data.length);
     let result = hasher.finish(false);
@@ -827,28 +789,16 @@ let Utils = {
 
     if (!ext) {
       return header;
     }
 
     return header += ', ext="' + ext +'"';
   },
 
-  makeURI: function Weave_makeURI(URIString) {
-    if (!URIString)
-      return null;
-    try {
-      return Services.io.newURI(URIString, null, null);
-    } catch (e) {
-      let log = Log4Moz.repository.getLogger("Sync.Utils");
-      log.debug("Could not create URI: " + Utils.exceptionStr(e));
-      return null;
-    }
-  },
-
   /**
    * Load a json object from disk
    *
    * @param filePath
    *        Json file path load from weave/[filePath].json
    * @param that
    *        Object to use for logging and "this" for callback
    * @param callback
@@ -912,69 +862,16 @@ let Utils = {
     let is = this._utf8Converter.convertToInputStream(out);
     NetUtil.asyncCopy(is, fos, function (result) {
       if (typeof callback == "function") {
         callback.call(that);        
       }
     });
   },
 
-  /**
-   * Execute a function on the next event loop tick.
-   * 
-   * @param callback
-   *        Function to invoke.
-   * @param thisObj [optional]
-   *        Object to bind the callback to.
-   */
-  nextTick: function nextTick(callback, thisObj) {
-    if (thisObj) {
-      callback = callback.bind(thisObj);
-    }
-    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
-  },
-
-  /**
-   * Return a timer that is scheduled to call the callback after waiting the
-   * provided time or as soon as possible. The timer will be set as a property
-   * of the provided object with the given timer name.
-   */
-  namedTimer: function delay(callback, wait, thisObj, name) {
-    if (!thisObj || !name) {
-      throw "You must provide both an object and a property name for the timer!";
-    }
-
-    // Delay an existing timer if it exists
-    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
-      thisObj[name].delay = wait;
-      return;
-    }
-
-    // Create a special timer that we can add extra properties
-    let timer = {};
-    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-
-    // Provide an easy way to clear out the timer
-    timer.clear = function() {
-      thisObj[name] = null;
-      timer.cancel();
-    };
-
-    // Initialize the timer with a smart callback
-    timer.initWithCallback({
-      notify: function notify() {
-        // Clear out the timer once it's been triggered
-        timer.clear();
-        callback.call(thisObj, timer);
-      }
-    }, wait, timer.TYPE_ONE_SHOT);
-
-    return thisObj[name] = timer;
-  },
-
   getIcon: function(iconUri, defaultIcon) {
     try {
       let iconURI = Utils.makeURI(iconUri);
       return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
     }
     catch(ex) {}
 
     // Just give the provided default icon or the system's default
--- a/services/sync/tests/unit/head_appinfo.js
+++ b/services/sync/tests/unit/head_appinfo.js
@@ -44,15 +44,15 @@ registrar.registerFactory(Components.ID(
                           XULAppInfoFactory);
 
 
 // Register resource aliases. Normally done in SyncComponents.manifest.
 function addResourceAlias() {
   Cu.import("resource://gre/modules/Services.jsm");
   const resProt = Services.io.getProtocolHandler("resource")
                           .QueryInterface(Ci.nsIResProtocolHandler);
-  let uri;
-  uri = Services.io.newURI("resource:///modules/services-sync/", null, null);
-  resProt.setSubstitution("services-sync", uri);
-  uri = Services.io.newURI("resource:///modules/services-crypto/", null, null);
-  resProt.setSubstitution("services-crypto", uri);
+  for each (let s in ["common", "sync", "crypto"]) {
+    let uri = Services.io.newURI("resource:///modules/services-" + s + "/", null,
+                                 null);
+    resProt.setSubstitution("services-" + s, uri);
+  }
 }
 addResourceAlias();
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 let btoa;
 let atob;
 
 let provider = {
@@ -35,52 +36,18 @@ function waitForZeroTimer(callback) {
       Utils.nextTick(wait);
       return;
     }
     callback();
   }
   timer = Utils.namedTimer(wait, 150, {}, "timer");
 }
 
-btoa = Cu.import("resource://services-sync/log4moz.js").btoa;
-atob = Cu.import("resource://services-sync/log4moz.js").atob;
-function getTestLogger(component) {
-  return Log4Moz.repository.getLogger("Testing");
-}
-
-function initTestLogging(level) {
-  function LogStats() {
-    this.errorsLogged = 0;
-  }
-  LogStats.prototype = {
-    format: function BF_format(message) {
-      if (message.level == Log4Moz.Level.Error)
-        this.errorsLogged += 1;
-      return message.loggerName + "\t" + message.levelDesc + "\t" +
-        message.message + "\n";
-    }
-  };
-  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
-
-  var log = Log4Moz.repository.rootLogger;
-  var logStats = new LogStats();
-  var appender = new Log4Moz.DumpAppender(logStats);
-
-  if (typeof(level) == "undefined")
-    level = "Debug";
-  getTestLogger().level = Log4Moz.Level[level];
-
-  log.level = Log4Moz.Level.Trace;
-  appender.level = Log4Moz.Level.Trace;
-  // Overwrite any other appenders (e.g. from previous incarnations)
-  log.ownAppenders = [appender];
-  log.updateAppenders();
-
-  return logStats;
-}
+btoa = Cu.import("resource://services-common/log4moz.js").btoa;
+atob = Cu.import("resource://services-common/log4moz.js").atob;
 
 // This is needed for loadAddonTestFunctions().
 let gGlobalScope = this;
 
 function ExtensionsTestPath(path) {
   if (path[0] != "/") {
     throw Error("Path must begin with '/': " + path);
   }
@@ -293,31 +260,19 @@ function ensureThrows(func) {
       func.apply(this, arguments);
     } catch (ex) {
       do_throw(ex);
     }
   };
 }
 
 
-/**
- * Print some debug message to the console. All arguments will be printed,
- * separated by spaces.
- *
- * @param [arg0, arg1, arg2, ...]
- *        Any number of arguments to print out
- * @usage _("Hello World") -> prints "Hello World"
- * @usage _(1, 2, 3) -> prints "1 2 3"
- */
-let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
-
 _("Setting the identity for passphrase");
 Cu.import("resource://services-sync/identity.js");
 
-
 /*
  * Test setup helpers.
  */
 
 // Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
 function encryptPayload(cleartext) {
   if (typeof cleartext == "object") {
     cleartext = JSON.stringify(cleartext);
@@ -329,42 +284,16 @@ function encryptPayload(cleartext) {
 }
 
 function generateNewKeys(collections) {
   let wbo = CollectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
   CollectionKeys.setContents(wbo.cleartext, modified);
 }
 
-function do_check_empty(obj) {
-  do_check_attribute_count(obj, 0);
-}
-
-function do_check_attribute_count(obj, c) {
-  do_check_eq(c, Object.keys(obj).length);
-}
-
-function do_check_throws(aFunc, aResult, aStack)
-{
-  if (!aStack) {
-    try {
-      // We might not have a 'Components' object.
-      aStack = Components.stack.caller;
-    } catch (e) {}
-  }
-
-  try {
-    aFunc();
-  } catch (e) {
-    do_check_eq(e.result, aResult, aStack);
-    return;
-  }
-  do_throw("Expected result " + aResult + ", none thrown.", aStack);
-}
-
 /*
  * A fake engine implementation.
  * This is used all over the place.
  *
  * Complete with record, store, and tracker implementations.
  */
 
 function RotaryRecord(collection, id) {
@@ -482,9 +411,9 @@ deepCopy: function deepCopy(thing, noSor
     let props = [p for (p in thing)];
     if (!noSort){
       props = props.sort();
     }
     props.forEach(function(k) ret[k] = deepCopy(thing[k], noSort));
   }
 
   return ret;
-};
\ No newline at end of file
+};
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,15 +1,15 @@
 const Cm = Components.manager;
 
 const TEST_CLUSTER_URL = "http://localhost:8080/";
 const TEST_SERVER_URL  = "http://localhost:8080/";
 
 // Shared logging for all HTTP server functions.
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
 const SYNC_API_VERSION = "1.1";
 
 // Use the same method that record.js does, which mirrors the server.
 // The server returns timestamps with 1/100 sec granularity. Note that this is
 // subject to change: see Bug 650435.
 function new_timestamp() {
   return Math.round(Date.now() / 10) / 100;
@@ -21,49 +21,16 @@ function return_timestamp(request, respo
   }
   let body = "" + timestamp;
   response.setHeader("X-Weave-Timestamp", body);
   response.setStatusLine(request.httpVersion, 200, "OK");
   response.bodyOutputStream.write(body, body.length);
   return timestamp;
 }
 
-function httpd_setup (handlers, port) {
-  let port   = port || 8080;
-  let server = new nsHttpServer();
-  for (let path in handlers) {
-    server.registerPathHandler(path, handlers[path]);
-  }
-  try {
-    server.start(port);
-  } catch (ex) {
-    _("==========================================");
-    _("Got exception starting HTTP server on port " + port);
-    _("Error: " + Utils.exceptionStr(ex));
-    _("Is there a process already listening on port " + port + "?");
-    _("==========================================");
-    do_throw(ex);
-  }
-
-  return server;
-}
-
-function httpd_handler(statusCode, status, body) {
-  return function handler(request, response) {
-    // Allow test functions to inspect the request.
-    request.body = readBytesFromInputStream(request.bodyInputStream);
-    handler.request = request;
-
-    response.setStatusLine(request.httpVersion, statusCode, status);
-    if (body) {
-      response.bodyOutputStream.write(body, body.length);
-    }
-  };
-}
-
 function basic_auth_header(user, password) {
   return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
 }
 
 function basic_auth_matches(req, user, password) {
   if (!req.hasHeader("Authorization")) {
     return false;
   }
@@ -80,31 +47,16 @@ function httpd_basic_auth_handler(body, 
     body = "This path exists and is protected - failed";
     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
   }
   response.bodyOutputStream.write(body, body.length);
 }
 
 /*
- * Read bytes string from an nsIInputStream.  If 'count' is omitted,
- * all available input is read.
- */
-function readBytesFromInputStream(inputStream, count) {
-  var BinaryInputStream = Components.Constructor(
-      "@mozilla.org/binaryinputstream;1",
-      "nsIBinaryInputStream",
-      "setInputStream");
-  if (!count) {
-    count = inputStream.available();
-  }
-  return new BinaryInputStream(inputStream).readBytes(count);
-}
-
-/*
  * Represent a WBO on the server
  */
 function ServerWBO(id, initialPayload, modified) {
   if (!id) {
     throw "No ID for ServerWBO!";
   }
   this.id = id;
   if (!initialPayload) {
@@ -1039,56 +991,8 @@ function serverForUsers(users, contents,
   let server = new SyncServer(callback);
   for (let [user, pass] in Iterator(users)) {
     server.registerUser(user, pass);
     server.createContents(user, contents);
   }
   server.start();
   return server;
 }
-
-/**
- * Proxy auth helpers.
- */
-
-/**
- * Fake a PAC to prompt a channel replacement.
- */
-let PACSystemSettings = {
-  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
-  contractID: "@mozilla.org/system-proxy-settings;1",
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
-                                         Ci.nsISystemProxySettings]),
-
-  createInstance: function createInstance(outer, iid) {
-    if (outer) {
-      throw Cr.NS_ERROR_NO_AGGREGATION;
-    }
-    return this.QueryInterface(iid);
-  },
-
-  lockFactory: function lockFactory(lock) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-  
-  // Replace this URI for each test to avoid caching. We want to ensure that
-  // each test gets a completely fresh setup.
-  PACURI: null,
-  getProxyForURI: function getProxyForURI(aURI) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  }
-};
-
-function installFakePAC() {
-  _("Installing fake PAC.");
-  Cm.nsIComponentRegistrar
-    .registerFactory(PACSystemSettings.CID,
-                     "Fake system proxy-settings",
-                     PACSystemSettings.contractID,
-                     PACSystemSettings);
-}
-
-function uninstallFakePAC() {
-  _("Uninstalling fake PAC.");
-  let CID = PACSystemSettings.CID;
-  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
-}
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-sync/addonsreconciler.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/service.js");
 
 let prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
 
 loadAddonTestFunctions();
 startupManager();
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 const HTTP_PORT = 8888;
 
 let prefs = new Preferences();
 
 Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
 loadAddonTestFunctions();
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -1,13 +1,13 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 Engines.register(BookmarksEngine);
 var syncTesting = new SyncTestingInfrastructure();
 
--- a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
+++ b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that Sync can correctly handle a legacy microsummary record
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const GENERATORURI_ANNO = "microsummary/generatorURI";
 const STATICTITLE_ANNO = "bookmarks/staticTitle";
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 function prepareBookmarkItem(collection, id) {
   let b = new Bookmark(collection, id);
   b.cleartext.stuff = "my payload here";
   return b;
 }
 
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 var IOService = Cc["@mozilla.org/network/io-service;1"]
                 .getService(Ci.nsIIOService);
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -2,17 +2,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 add_test(function test_locally_changed_keys() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let hmacErrorCount = 0;
   function counting(f) {
     return function() {
       hmacErrorCount++;
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/util.js");
 
 
 function SteamStore() {
   Store.call(this, "Steam");
   this.wasWiped = false;
 }
 SteamStore.prototype = {
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const logsdir            = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 const CLEANUP_DELAY      = 1000; // delay to age files for cleanup (ms)
 const DELAY_BUFFER       = 50; // buffer for timers on different OS platforms
 
 const PROLONGED_ERROR_DURATION =
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/forms.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = new FormEngine()._tracker;
   do_check_empty(tracker.changedIDs);
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   try {
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
 const TIMESTAMP2 = (Date.now() - 6592903) * 1000;
 const TIMESTAMP3 = (Date.now() - 123894) * 1000;
 
 function queryPlaces(uri, options) {
   let query = PlacesUtils.history.getNewQuery();
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -54,17 +54,17 @@ add_test(function test_url_parsing() {
   parts = server.storageRE.exec("storage");
   let [all, storage, collection, id] = parts;
   do_check_eq(all, "storage");
   do_check_eq(collection, undefined);
 
   run_next_test();
 });
 
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/rest.js");
 function localRequest(path) {
   _("localRequest: " + path);
   let url = "http://127.0.0.1:8080" + path;
   _("url: " + url);
   return new RESTRequest(url);
 }
 
 add_test(function test_basic_http() {
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/jpakeclient.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const JPAKE_LENGTH_SECRET     = 8;
 const JPAKE_LENGTH_CLIENTID   = 256;
 const KEYEXCHANGE_VERSION     = 3;
@@ -185,17 +185,18 @@ function run_test() {
   // sure the J-PAKE requests don't include those data.
   setBasicCredentials("johndoe", "ilovejane");
 
   server = httpd_setup({"/new_channel": server_new_channel,
                         "/report":      server_report});
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   run_next_test();
 }
 
 
 add_test(function test_success_receiveNoPIN() {
   _("Test a successful exchange started by receiveNoPIN().");
 
   let snd = new JPAKEClient({
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -1,27 +1,23 @@
 const modules = [
                  "addonsreconciler.js",
-                 "async.js",
                  "constants.js",
                  "engines/addons.js",
                  "engines/bookmarks.js",
                  "engines/clients.js",
                  "engines/forms.js",
                  "engines/history.js",
                  "engines/passwords.js",
                  "engines/prefs.js",
                  "engines/tabs.js",
                  "engines.js",
-                 "ext/Observers.js",
-                 "ext/Preferences.js",
                  "identity.js",
                  "jpakeclient.js",
                  "keys.js",
-                 "log4moz.js",
                  "main.js",
                  "notifications.js",
                  "policies.js",
                  "record.js",
                  "resource.js",
                  "rest.js",
                  "service.js",
                  "status.js",
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -2,22 +2,22 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Test that node reassignment responses are respected on all kinds of " +
   "requests.");
 
 // Don't sync any engines by default.
 Svc.DefaultPrefs.set("registerEngines", "")
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/policies.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level  = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level      = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.RESTRequest").level   = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level       = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/history.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 
 const kDBName = "places.sqlite";
 const storageSvc = Cc["@mozilla.org/storage/service;1"]
                      .getService(Ci.mozIStorageService);
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function makePersona(id) {
   return {
     id: id || Math.random().toString(),
--- a/services/sync/tests/unit/test_prefs_tracker.js
+++ b/services/sync/tests/unit/test_prefs_tracker.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function run_test() {
   let engine = new PrefsEngine();
   let tracker = engine._tracker;
   let prefs = new Preferences();
 
   try {
 
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let cryptoWrap;
 
 function crypted_resource_handler(metadata, response) {
   let obj = {id: "resource",
              modified: cryptoWrap.modified,
              payload: JSON.stringify(cryptoWrap.payload)};
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 let logger;
 
 let fetched = false;
 function server_open(metadata, response) {
   let body;
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 const RES_UPLOAD_URL = "http://localhost:8080/upload";
 const RES_HEADERS_URL = "http://localhost:8080/headers";
 
 let logger;
 
--- a/services/sync/tests/unit/test_service_changePassword.js
+++ b/services/sync/tests/unit/test_service_changePassword.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
 
   run_next_test();
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -5,17 +5,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/engines/tabs.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 Engines.register(TabEngine);
 
 add_test(function v4_upgrade() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_getStorageInfo.js
+++ b/services/sync/tests/unit/test_service_getStorageInfo.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 let collections = {steam:  65.11328,
                    petrol: 82.488281,
                    diesel: 2.25488281};
 
 function run_test() {
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/policies.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane") ||
--- a/services/sync/tests/unit/test_service_migratePrefs.js
+++ b/services/sync/tests/unit/test_service_migratePrefs.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function test_migrate_logging() {
   _("Testing log pref migration.");
   Svc.Prefs.set("log.appender.debugLog", "Warn");
   Svc.Prefs.set("log.appender.debugLog.enabled", true);
   do_check_true(Svc.Prefs.get("log.appender.debugLog.enabled"));
   do_check_eq(Svc.Prefs.get("log.appender.file.level"), "Trace");
   do_check_eq(Svc.Prefs.get("log.appender.file.logOnSuccess"), false);
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
 
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let guidSvc = new FakeGUIDService();
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane")) {
       handler(request, response);
--- a/services/sync/tests/unit/test_syncstoragerequest.js
+++ b/services/sync/tests/unit/test_syncstoragerequest.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const STORAGE_REQUEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
--- a/services/sync/tests/unit/test_utils_lazyStrings.js
+++ b/services/sync/tests/unit/test_utils_lazyStrings.js
@@ -1,10 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 
 function run_test() {
     let fn = Utils.lazyStrings("sync");
     do_check_eq(typeof fn, "function");
     let bundle = fn();
     do_check_true(bundle instanceof StringBundle);
     let url = bundle.url;
     do_check_eq(url, "chrome://weave/locale/services/sync.properties");
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,14 +1,12 @@
 [DEFAULT]
-head = head_appinfo.js head_helpers.js head_http_server.js
+head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js
 tail =
 
-[test_load_modules.js]
-
 # The manifest is roughly ordered from low-level to high-level. When making
 # systemic sweeping changes, this makes it easier to identify errors closer to
 # the source.
 
 # Ensure we can import everything.
 [test_load_modules.js]
 
 # util contains a bunch of functionality used throughout.
@@ -22,33 +20,24 @@ tail =
 [test_utils_getErrorString.js]
 [test_utils_getIcon.js]
 [test_utils_hkdfExpand.js]
 [test_utils_httpmac.js]
 [test_utils_json.js]
 [test_utils_lazyStrings.js]
 [test_utils_lock.js]
 [test_utils_makeGUID.js]
-[test_utils_makeURI.js]
-[test_utils_namedTimer.js]
 [test_utils_notify.js]
 [test_utils_passphrase.js]
 [test_utils_pbkdf2.js]
 [test_utils_sha1.js]
-[test_utils_stackTrace.js]
 [test_utils_utf8.js]
 
 # We have a number of other libraries that are pretty much standalone.
-[test_Observers.js]
-[test_Preferences.js]
-[test_async_chain.js]
-[test_async_querySpinningly.js]
 [test_httpd_sync_server.js]
-[test_log4moz.js]
-[test_restrequest.js]
 [test_jpakeclient.js]
 # Bug 618233: this test produces random failures on Windows 7.
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "win" || os == "android"
 
 # HTTP layers.
 [test_resource.js]
 [test_resource_async.js]
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -37,17 +37,17 @@
 
 let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://tps/logger.jsm");
 
 const ADDONSGETURL = 'http://127.0.0.1:4567/';
 const STATE_ENABLED = 1;
 const STATE_DISABLED = 2;
 
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/modules/bookmarks.jsm
@@ -45,17 +45,17 @@ var EXPORTED_SYMBOLS = ["PlacesItem", "B
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/modules/history.jsm
@@ -44,17 +44,17 @@ var EXPORTED_SYMBOLS = ["HistoryEntry", 
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
 CU.import("resource://tps/logger.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpHistory = function TPS_History__DumpHistory() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -42,17 +42,17 @@
 
 let EXPORTED_SYMBOLS = ["TPS"];
 
 const {classes: CC, interfaces: CI, utils: CU} = Components;
 
 CU.import("resource://services-sync/service.js");
 CU.import("resource://services-sync/constants.js");
 CU.import("resource://services-sync/engines.js");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 CU.import("resource://services-sync/util.js");
 CU.import("resource://gre/modules/XPCOMUtils.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://tps/addons.jsm");
 CU.import("resource://tps/bookmarks.jsm");
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://tps/passwords.jsm");
 CU.import("resource://tps/history.jsm");
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -62,16 +62,17 @@ skip-if = os == "android"
 [include:extensions/cookie/test/unit/xpcshell.ini]
 [include:storage/test/unit/xpcshell.ini]
 [include:rdf/tests/unit/xpcshell.ini]
 [include:gfx/tests/unit/xpcshell.ini]
 [include:widget/tests/unit/xpcshell.ini]
 [include:content/base/test/unit/xpcshell.ini]
 [include:content/test/unit/xpcshell.ini]
 [include:toolkit/components/url-classifier/tests/unit/xpcshell.ini]
+[include:services/common/tests/unit/xpcshell.ini]
 [include:services/crypto/tests/unit/xpcshell.ini]
 [include:services/crypto/components/tests/unit/xpcshell.ini]
 [include:services/sync/tests/unit/xpcshell.ini]
 # Bug 676978: tests hang on Android 
 skip-if = os == "android"
 [include:browser/components/dirprovider/tests/unit/xpcshell.ini]
 [include:browser/components/feeds/test/unit/xpcshell.ini]
 [include:browser/components/migration/tests/unit/xpcshell.ini]