Merge mozilla-central into mozilla-inbound
authorEhsan Akhgari <ehsan@mozilla.com>
Mon, 22 Oct 2012 17:05:46 -0400
changeset 111214 c4aecf0cb060ed245ff88447fd8d181109c82cf7
parent 111213 d04e584c197d4c1bfa2e2592d18499467a7cc957 (current diff)
parent 111120 acf7f73e8b18d1400d690cda2c2ce470aa081a14 (diff)
child 111215 48502b61a63e9c17df6914429101cf8a6b2253cf
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
milestone19.0a1
Merge mozilla-central into mozilla-inbound
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -647,18 +647,19 @@ HistoryMenu.prototype = {
     if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
         Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       menuitem.setAttribute("hidden", true);
       return;
     }
 
     // The tabs engine might never be inited (if services.sync.registerEngines
     // is modified), so make sure we avoid undefined errors.
-    let enabled = Weave.Service.isLoggedIn && Weave.Engines.get("tabs") &&
-                  Weave.Engines.get("tabs").enabled;
+    let enabled = Weave.Service.isLoggedIn &&
+                  Weave.Service.engineManager.get("tabs") &&
+                  Weave.Service.engineManager.get("tabs").enabled;
     menuitem.setAttribute("disabled", !enabled);
     menuitem.setAttribute("hidden", false);
 #endif
   },
 
   toggleRestoreLastSession: function PHM_toggleRestoreLastSession() {
     let restoreItem = this._rootElt.getElementsByClassName("restoreLastSession")[0];
 
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -207,17 +207,17 @@ let gSyncUI = {
 
   openServerStatus: function () {
     let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
     window.openUILinkIn(statusURL, "tab");
   },
 
   // Commands
   doSync: function SUI_doSync() {
-    setTimeout(function() Weave.ErrorHandler.syncAndReportErrors(), 0);
+    setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0);
   },
 
   handleToolbarButton: function SUI_handleStatusbarButton() {
     if (this._needsSetup())
       this.openSetup();
     else
       this.doSync();
   },
--- a/browser/base/content/sync/aboutSyncTabs.js
+++ b/browser/base/content/sync/aboutSyncTabs.js
@@ -125,17 +125,17 @@ let RemoteTabViewer = {
                                        , type: "folder"
                                        , URIList: URIs
                                        , hiddenRows: [ "description" ]
                                        }, window.top);
     }
   },
 
   _generateTabList: function() {
-    let engine = Weave.Engines.get("tabs");
+    let engine = Weave.Service.engineManager.get("tabs");
     let list = this._tabsList;
 
     // clear out existing richlistitems
     let count = list.getRowCount();
     if (count > 0) {
       for (let i = count - 1; i >= 0; i--)
         list.removeItemAt(i);
     }
@@ -150,17 +150,17 @@ let RemoteTabViewer = {
           return;
 
         seenURLs[url] = null;
 
         if (appendClient) {
           let attrs = {
             type: "client",
             clientName: client.clientName,
-            class: Weave.Clients.isMobile(client.id) ? "mobile" : "desktop"
+            class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop"
           };
           let clientEnt = this.createItem(attrs);
           list.appendChild(clientEnt);
           appendClient = false;
           clientEnt.disabled = true;
         }
         let attrs = {
           type:  "tab",
@@ -206,21 +206,21 @@ let RemoteTabViewer = {
       }
       catch (e) { /* Just use the default value of 0 */ }
       let now = Math.floor(Date.now() / 1000);
       if (now - lastFetch < 30)
         return false;
     }
 
     // if Clients hasn't synced yet this session, need to sync it as well
-    if (Weave.Clients.lastSync == 0)
-      Weave.Clients.sync();
+    if (Weave.Service.clientsEngine.lastSync == 0)
+      Weave.Service.clientsEngine.sync();
 
     // Force a sync only for the tabs engine
-    let engine = Weave.Engines.get("tabs");
+    let engine = Weave.Service.engineManager.get("tabs");
     engine.lastModified = null;
     engine.sync();
     Services.prefs.setIntPref("services.sync.lastTabFetch",
                               Math.floor(Date.now() / 1000));
 
     return true;
   },
 
--- a/browser/base/content/sync/addDevice.js
+++ b/browser/base/content/sync/addDevice.js
@@ -26,17 +26,17 @@ let gSyncAddDevice = {
                         pin2: this.pin3,
                         pin3: this.wizard.getButton("next")};
 
     this.throbber = document.getElementById("pairDeviceThrobber");
     this.errorRow = document.getElementById("errorRow");
 
     // Kick off a sync. That way the server will have the most recent data from
     // this computer and it will show up immediately on the new device.
-    Weave.SyncScheduler.scheduleNextSync(0);
+    Weave.Service.scheduler.scheduleNextSync(0);
   },
 
   onPageShow: function onPageShow() {
     this.wizard.getButton("back").hidden = true;
 
     switch (this.wizard.pageIndex) {
       case ADD_DEVICE_PAGE:
         this.onTextBoxInput();
@@ -45,17 +45,17 @@ let gSyncAddDevice = {
         this.pin1.focus();
         break;
       case SYNC_KEY_PAGE:
         this.wizard.canAdvance = false;
         this.wizard.canRewind = true;
         this.wizard.getButton("back").hidden = false;
         this.wizard.getButton("next").hidden = true;
         document.getElementById("weavePassphrase").value =
-          Weave.Utils.hyphenatePassphrase(Weave.Identity.syncKey);
+          Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
         break;
       case DEVICE_CONNECTED_PAGE:
         this.wizard.canAdvance = true;
         this.wizard.canRewind = false;
         this.wizard.getButton("cancel").hidden = true;
         break;
     }
   },
@@ -75,29 +75,29 @@ let gSyncAddDevice = {
   startTransfer: function startTransfer() {
     this.errorRow.hidden = true;
     // When onAbort is called, Weave may already be gone.
     const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
 
     let self = this;
     let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
       onPaired: function onPaired() {
-        let credentials = {account:   Weave.Identity.account,
-                           password:  Weave.Identity.basicPassword,
-                           synckey:   Weave.Identity.syncKey,
+        let credentials = {account:   Weave.Service.identity.account,
+                           password:  Weave.Service.identity.basicPassword,
+                           synckey:   Weave.Service.identity.syncKey,
                            serverURL: Weave.Service.serverURL};
         jpakeclient.sendAndComplete(credentials);
       },
       onComplete: function onComplete() {
         delete self._jpakeclient;
         self.wizard.pageIndex = DEVICE_CONNECTED_PAGE;
 
         // Schedule a Sync for soonish to fetch the data uploaded by the
         // device with which we just paired.
-        Weave.SyncScheduler.scheduleNextSync(Weave.SyncScheduler.activeInterval);
+        Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval);
       },
       onAbort: function onAbort(error) {
         delete self._jpakeclient;
 
         // Aborted by user, ignore.
         if (error == JPAKE_ERROR_USERABORT) {
           return;
         }
--- a/browser/base/content/sync/genericChange.js
+++ b/browser/base/content/sync/genericChange.js
@@ -66,17 +66,17 @@ let Change = {
           introText.textContent = this._str("new.recoverykey.introText");
           this._dialog.getButton("finish").label
             = this._str("new.recoverykey.acceptButton");
         }
         else {
           document.getElementById("generatePassphraseButton").hidden = false;
           document.getElementById("passphraseBackupButtons").hidden = false;
           this._passphraseBox.setAttribute("readonly", "true");
-          let pp = Weave.Identity.syncKey;
+          let pp = Weave.Service.identity.syncKey;
           if (Weave.Utils.isPassphrase(pp))
              pp = Weave.Utils.hyphenatePassphrase(pp);
           this._passphraseBox.value = pp;
           this._passphraseBox.focus();
           document.title = this._str("change.recoverykey.title");
           introText.textContent = this._str("change.synckey.introText2");
           warningText.textContent = this._str("change.recoverykey.warningText");
           this._dialog.getButton("finish").label
@@ -153,21 +153,21 @@ let Change = {
     let passphrase = Weave.Utils.generatePassphrase();
     this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase);
     this._dialog.getButton("finish").disabled = false;
   },
 
   doChangePassphrase: function Change_doChangePassphrase() {
     let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
     if (this._updatingPassphrase) {
-      Weave.Identity.syncKey = pp;
+      Weave.Service.identity.syncKey = pp;
       if (Weave.Service.login()) {
         this._updateStatus("change.recoverykey.success", "success");
         Weave.Service.persistLogin();
-        Weave.SyncScheduler.delayedAutoConnect(0);
+        Weave.Service.scheduler.delayedAutoConnect(0);
       }
       else {
         this._updateStatus("new.passphrase.status.incorrect", "error");
       }
     }
     else {
       this._updateStatus("change.recoverykey.label", "active");
 
@@ -177,17 +177,17 @@ let Change = {
         this._updateStatus("change.recoverykey.error", "error");
     }
 
     return false;
   },
 
   doChangePassword: function Change_doChangePassword() {
     if (this._currentPasswordInvalid) {
-      Weave.Identity.basicPassword = this._firstBox.value;
+      Weave.Service.identity.basicPassword = this._firstBox.value;
       if (Weave.Service.login()) {
         this._updateStatus("change.password.status.success", "success");
         Weave.Service.persistLogin();
       }
       else {
         this._updateStatus("new.password.status.incorrect", "error");
       }
     }
--- a/browser/base/content/sync/progress.js
+++ b/browser/base/content/sync/progress.js
@@ -47,17 +47,17 @@ function onEngineSync(subject, topic, da
   // and evaluate how many engines are enabled when the first "real" engine
   // syncs.
   if (data == "clients") {
     return;
   }
 
   if (!gCounter &&
       Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) {
-    gProgressBar.max = Weave.Engines.getEnabled().length;
+    gProgressBar.max = Weave.Service.engineManager.getEnabled().length;
   }
 
   gCounter += 1;
   gProgressBar.setAttribute("value", gCounter);
 }
 
 function onServiceSync(subject, topic, data) {
   // To address the case where 0 engines are synced, we will fill the
--- a/browser/base/content/sync/quota.js
+++ b/browser/base/content/sync/quota.js
@@ -65,17 +65,17 @@ let gSyncQuota = {
       this._quota_req.abort();
     }
     return true;
   },
 
   onAccept: function onAccept() {
     let engines = gUsageTreeView.getEnginesToDisable();
     for each (let engine in engines) {
-      Weave.Engines.get(engine).enabled = false;
+      Weave.Service.engineManager.get(engine).enabled = false;
     }
     if (engines.length) {
       // The 'Weave' object will disappear once the window closes.
       let Service = Weave.Service;
       Weave.Utils.nextTick(function() { Service.sync(); });
     }
     return this.onCancel();
   },
@@ -95,17 +95,17 @@ let gUsageTreeView = {
   /*
    * Internal data structures underlaying the tree.
    */
   _collections: [],
   _byname: {},
 
   init: function init() {
     let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label");
-    for each (let engine in Weave.Engines.getEnabled()) {
+    for each (let engine in Weave.Service.engineManager.getEnabled()) {
       if (this._ignored[engine.name])
         continue;
 
       // Some engines use the same pref, which means they can only be turned on
       // and off together. We need to combine them here as well.
       let existing = this._byname[engine.prefName];
       if (existing) {
         existing.engines.push(engine.name);
--- a/browser/base/content/sync/setup.js
+++ b/browser/base/content/sync/setup.js
@@ -55,20 +55,20 @@ var gSyncSetup = {
   get _usingMainServers() {
     if (this._settingUpNew)
       return document.getElementById("server").selectedIndex == 0;
     return document.getElementById("existingServer").selectedIndex == 0;
   },
 
   init: function () {
     let obs = [
-      ["weave:service:changepph:finish", "onResetPassphrase"],
-      ["weave:service:login:start",  "onLoginStart"],
-      ["weave:service:login:error",  "onLoginEnd"],
-      ["weave:service:login:finish", "onLoginEnd"]];
+      ["weave:service:change-passphrase", "onResetPassphrase"],
+      ["weave:service:login:start",       "onLoginStart"],
+      ["weave:service:login:error",       "onLoginEnd"],
+      ["weave:service:login:finish",      "onLoginEnd"]];
 
     // Add the observers now and remove them on unload
     let self = this;
     let addRem = function(add) {
       obs.forEach(function([topic, func]) {
         //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
         //        of `this`. Fix in a followup. (bug 583347)
         if (add)
@@ -136,23 +136,23 @@ var gSyncSetup = {
     } else {
       this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE;
     }
   },
 
   resetPassphrase: function resetPassphrase() {
     // Apply the existing form fields so that
     // Weave.Service.changePassphrase() has the necessary credentials.
-    Weave.Identity.account = document.getElementById("existingAccountName").value;
-    Weave.Identity.basicPassword = document.getElementById("existingPassword").value;
+    Weave.Service.identity.account = document.getElementById("existingAccountName").value;
+    Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value;
 
     // Generate a new passphrase so that Weave.Service.login() will
     // actually do something.
     let passphrase = Weave.Utils.generatePassphrase();
-    Weave.Identity.syncKey = passphrase;
+    Weave.Service.identity.syncKey = passphrase;
 
     // Only open the dialog if username + password are actually correct.
     Weave.Service.login();
     if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE,
          Weave.LOGIN_FAILED_NO_PASSPHRASE,
          Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) {
       return;
     }
@@ -167,36 +167,36 @@ var gSyncSetup = {
     // according to the user's pref.
     Weave.Svc.Prefs.reset("firstSync");
     this.setupInitialSync();
     gSyncUtils.resetPassphrase(true);
   },
 
   onResetPassphrase: function () {
     document.getElementById("existingPassphrase").value =
-      Weave.Utils.hyphenatePassphrase(Weave.Identity.syncKey);
+      Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
     this.checkFields();
     this.wizard.advance();
   },
 
   onLoginStart: function () {
     this.toggleLoginFeedback(false);
   },
 
   onLoginEnd: function () {
     this.toggleLoginFeedback(true);
   },
 
   sendCredentialsAfterSync: function () {
     let send = function() {
       Services.obs.removeObserver("weave:service:sync:finish", send);
       Services.obs.removeObserver("weave:service:sync:error", send);
-      let credentials = {account:   Weave.Identity.account,
-                         password:  Weave.Identity.basicPassword,
-                         synckey:   Weave.Identity.syncKey,
+      let credentials = {account:   Weave.Service.identity.account,
+                         password:  Weave.Service.identity.basicPassword,
+                         synckey:   Weave.Service.identity.syncKey,
                          serverURL: Weave.Service.serverURL};
       this._jpakeclient.sendAndComplete(credentials);
     }.bind(this);
     Services.obs.addObserver("weave:service:sync:finish", send, false);
     Services.obs.addObserver("weave:service:sync:error", send, false);
   },
 
   toggleLoginFeedback: function (stop) {
@@ -325,17 +325,17 @@ var gSyncSetup = {
         else
           str = availCheck;
       }
     }
 
     this._setFeedbackMessage(feedback, valid, str);
     this.status.email = valid;
     if (valid)
-      Weave.Identity.account = value;
+      Weave.Service.identity.account = value;
     this.checkFields();
   },
 
   onPasswordChange: function () {
     let password = document.getElementById("weavePassword");
     let pwconfirm = document.getElementById("weavePasswordConfirm");
     let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm);
 
@@ -395,17 +395,17 @@ var gSyncSetup = {
           this.wizard.getButton("next").label =
             this._stringBundle.GetStringFromName("button.syncOptionsDone.label");
           this.wizard.getButton("next").removeAttribute("accesskey");
         }
         this.wizard.getButton("next").hidden = false;
         this.wizard.getButton("back").hidden = true;
         this.wizard.getButton("cancel").hidden = !this._resettingSync;
         this.wizard.getButton("extra1").hidden = true;
-        document.getElementById("syncComputerName").value = Weave.Clients.localName;
+        document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
         document.getElementById("syncOptions").collapsed = this._resettingSync;
         document.getElementById("mergeOptions").collapsed = this._settingUpNew;
         break;
       case OPTIONS_CONFIRM_PAGE:
         this.wizard.canRewind = true;
         this.wizard.canAdvance = true;
         this.wizard.getButton("back").label =
           this._stringBundle.GetStringFromName("button.syncOptionsCancel.label");
@@ -461,35 +461,35 @@ var gSyncSetup = {
           document.getElementById("weaveEmail").value);
         let challenge = getField("challenge");
         let response = getField("response");
 
         let error = Weave.Service.createAccount(email, password,
                                                 challenge, response);
 
         if (error == null) {
-          Weave.Identity.account = email;
-          Weave.Identity.basicPassword = password;
-          Weave.Identity.syncKey = Weave.Utils.generatePassphrase();
+          Weave.Service.identity.account = email;
+          Weave.Service.identity.basicPassword = password;
+          Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
           this._handleNoScript(false);
           Weave.Svc.Prefs.set("firstSync", "newAccount");
           this.wizardFinish();
           return false;
         }
 
         image.setAttribute("status", "error");
         label.value = Weave.Utils.getErrorString(error);
         return false;
       case EXISTING_ACCOUNT_LOGIN_PAGE:
-        Weave.Identity.account = Weave.Utils.normalizeAccount(
+        Weave.Service.identity.account = Weave.Utils.normalizeAccount(
           document.getElementById("existingAccountName").value);
-        Weave.Identity.basicPassword =
+        Weave.Service.identity.basicPassword =
           document.getElementById("existingPassword").value;
         let pp = document.getElementById("existingPassphrase").value;
-        Weave.Identity.syncKey = Weave.Utils.normalizePassphrase(pp);
+        Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp);
         if (Weave.Service.login()) {
           this.wizardFinish();
         }
         return false;
       case OPTIONS_PAGE:
         let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
         // No confirmation needed on new account setup or merge option
         // with existing account.
@@ -634,17 +634,18 @@ var gSyncSetup = {
   completePairing: function completePairing() {
     if (!this._jpakeclient) {
       // The channel was aborted while we were setting up the account
       // locally. XXX TODO should we do anything here, e.g. tell
       // the user on the last wizard page that it's ok, they just
       // have to pair again?
       return;
     }
-    let controller = new Weave.SendCredentialsController(this._jpakeclient);
+    let controller = new Weave.SendCredentialsController(this._jpakeclient,
+                                                         Weave.Service);
     this._jpakeclient.controller = controller;
   },
 
   startEasySetup: function () {
     // Don't do anything if we have a client already (e.g. we went to
     // Sync Options and just came back).
     if (this._jpakeclient)
       return;
@@ -658,19 +659,19 @@ var gSyncSetup = {
         document.getElementById("easySetupPIN1").value = pin.slice(0, 4);
         document.getElementById("easySetupPIN2").value = pin.slice(4, 8);
         document.getElementById("easySetupPIN3").value = pin.slice(8);
       },
 
       onPairingStart: function onPairingStart() {},
 
       onComplete: function onComplete(credentials) {
-        Weave.Identity.account = credentials.account;
-        Weave.Identity.basicPassword = credentials.password;
-        Weave.Identity.syncKey = credentials.synckey;
+        Weave.Service.identity.account = credentials.account;
+        Weave.Service.identity.basicPassword = credentials.password;
+        Weave.Service.identity.syncKey = credentials.synckey;
         Weave.Service.serverURL = credentials.serverURL;
         gSyncSetup.wizardFinish();
       },
 
       onAbort: function onAbort(error) {
         delete self._jpakeclient;
 
         // Ignore if wizard is aborted.
@@ -865,17 +866,17 @@ var gSyncSetup = {
     switch (desc) {
       case 1:
         if (this._case1Setup)
           break;
 
         let places_db = PlacesUtils.history
                                    .QueryInterface(Ci.nsPIPlacesDatabase)
                                    .DBConnection;
-        if (Weave.Engines.get("history").enabled) {
+        if (Weave.Service.engineManager.get("history").enabled) {
           let daysOfHistory = 0;
           let stm = places_db.createStatement(
             "SELECT ROUND(( " +
               "strftime('%s','now','localtime','utc') - " +
               "( " +
                 "SELECT visit_date FROM moz_historyvisits " +
                 "ORDER BY visit_date ASC LIMIT 1 " +
                 ")/1000000 " +
@@ -888,17 +889,17 @@ var gSyncSetup = {
             PluralForm.get(daysOfHistory,
                            this._stringBundle.GetStringFromName("historyDaysCount.label"))
                       .replace("%S", daysOfHistory)
                       .replace("#1", daysOfHistory);
         } else {
           document.getElementById("historyCount").hidden = true;
         }
 
-        if (Weave.Engines.get("bookmarks").enabled) {
+        if (Weave.Service.engineManager.get("bookmarks").enabled) {
           let bookmarks = 0;
           let stm = places_db.createStatement(
             "SELECT count(*) AS bookmarks " +
             "FROM moz_bookmarks b " +
             "LEFT JOIN moz_bookmarks t ON " +
             "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag");
           stm.params.tag = PlacesUtils.tagsFolderId;
           if (stm.executeStep())
@@ -908,34 +909,35 @@ var gSyncSetup = {
             PluralForm.get(bookmarks,
                            this._stringBundle.GetStringFromName("bookmarksCount.label"))
                       .replace("%S", bookmarks)
                       .replace("#1", bookmarks);
         } else {
           document.getElementById("bookmarkCount").hidden = true;
         }
 
-        if (Weave.Engines.get("passwords").enabled) {
+        if (Weave.Service.engineManager.get("passwords").enabled) {
           let logins = Services.logins.getAllLogins({});
           // Support %S for historical reasons (see bug 600141)
           document.getElementById("passwordCount").value =
             PluralForm.get(logins.length,
                            this._stringBundle.GetStringFromName("passwordsCount.label"))
                       .replace("%S", logins.length)
                       .replace("#1", logins.length);
         } else {
           document.getElementById("passwordCount").hidden = true;
         }
 
-        if (!Weave.Engines.get("prefs").enabled) {
+        if (!Weave.Service.engineManager.get("prefs").enabled) {
           document.getElementById("prefsWipe").hidden = true;
         }
 
-        if (Weave.Engines.get("addons").enabled) {
-          let ids = Weave.Engines.get("addons")._store.getAllIDs();
+        let addonsEngine = Weave.Service.engineManager.get("addons");
+        if (addonsEngine.enabled) {
+          let ids = addonsEngine._store.getAllIDs();
           let blessedcount = 0;
           for each (let i in ids) {
             if (i) {
               blessedcount++;
             }
           }
           // bug 600141 does not apply, as this does not have to support existing strings
           document.getElementById("addonCount").value =
@@ -955,19 +957,19 @@ var gSyncSetup = {
         function appendNode(label) {
           let box = document.getElementById("clientList");
           let node = document.createElement("label");
           node.setAttribute("value", label);
           node.setAttribute("class", "data indent");
           box.appendChild(node);
         }
 
-        for each (let name in Weave.Clients.stats.names) {
+        for each (let name in Weave.Service.clientsEngine.stats.names) {
           // Don't list the current client
-          if (name == Weave.Clients.localName)
+          if (name == Weave.Service.clientsEngine.localName)
             continue;
 
           // Only show the first several client names
           if (++count <= 5)
             appendNode(name);
         }
         if (count > 5) {
           // Support %S for historical reasons (see bug 600141)
--- a/browser/base/content/sync/utils.js
+++ b/browser/base/content/sync/utils.js
@@ -22,18 +22,18 @@ let gSyncUtils = {
       Services.wm.getMostRecentWindow("navigator:browser")
               .openUILinkIn(url, "tab");
     else
       openUILinkIn(url, "tab");
   },
 
   changeName: function changeName(input) {
     // Make sure to update to a modified name, e.g., empty-string -> default
-    Weave.Clients.localName = input.value;
-    input.value = Weave.Clients.localName;
+    Weave.Service.clientsEngine.localName = input.value;
+    input.value = Weave.Service.clientsEngine.localName;
   },
 
   openChange: function openChange(type, duringSetup) {
     // Just re-show the dialog if it's already open
     let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type);
     if (openedDialog != null) {
       openedDialog.focus();
       return;
@@ -188,21 +188,21 @@ let gSyncUtils = {
   validatePassword: function (el1, el2) {
     let valid = false;
     let val1 = el1.value;
     let val2 = el2 ? el2.value : "";
     let error = "";
 
     if (!el2)
       valid = val1.length >= Weave.MIN_PASS_LENGTH;
-    else if (val1 && val1 == Weave.Identity.username)
+    else if (val1 && val1 == Weave.Service.identity.username)
       error = "change.password.pwSameAsUsername";
-    else if (val1 && val1 == Weave.Identity.account)
+    else if (val1 && val1 == Weave.Service.identity.account)
       error = "change.password.pwSameAsEmail";
-    else if (val1 && val1 == Weave.Identity.basicPassword)
+    else if (val1 && val1 == Weave.Service.identity.basicPassword)
       error = "change.password.pwSameAsPassword";
     else if (val1 && val2) {
       if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
         valid = true;
       else if (val1.length < Weave.MIN_PASS_LENGTH)
         error = "change.password.tooShort";
       else if (val1 != val2)
         error = "change.password.mismatch";
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -136,17 +136,17 @@ BrowserGlue.prototype = {
     let delay = 3;
     let browserEnum = Services.wm.getEnumerator("navigator:browser");
     while (browserEnum.hasMoreElements()) {
       delay += browserEnum.getNext().gBrowser.tabs.length;
     }
     delay = delay <= MAX_DELAY ? delay : MAX_DELAY;
 
     Cu.import("resource://services-sync/main.js");
-    Weave.SyncScheduler.delayedAutoConnect(delay);
+    Weave.Service.scheduler.delayedAutoConnect(delay);
   },
 #endif
 
   // nsIObserver implementation 
   observe: function BG_observe(subject, topic, data) {
     switch (topic) {
       case "xpcom-shutdown":
         this._dispose();
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-Components.utils.import("resource://services-sync/service.js");
+Components.utils.import("resource://services-sync/main.js");
 
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
 const PAGE_NEEDS_UPDATE = 2;
 
 let gSyncPane = {
   _stringBundle: null,
   prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
@@ -60,18 +60,18 @@ let gSyncPane = {
     if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
         Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
     } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
                Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
       this.needsUpdate();
     } else {
       this.page = PAGE_HAS_ACCOUNT;
-      document.getElementById("accountName").value = Weave.Identity.account;
-      document.getElementById("syncComputerName").value = Weave.Clients.localName;
+      document.getElementById("accountName").value = Weave.Service.identity.account;
+      document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP").hidden = this._usingCustomServer;
     }
   },
 
   startOver: function (showDialog) {
     if (showDialog) {
       let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
                   Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-Components.utils.import("resource://services-sync/service.js");
+Components.utils.import("resource://services-sync/main.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
 const PAGE_NEEDS_UPDATE = 2;
 
 let gSyncPane = {
   _stringBundle: null,
@@ -61,18 +61,18 @@ let gSyncPane = {
     if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
         Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
     } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
                Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
       this.needsUpdate();
     } else {
       this.page = PAGE_HAS_ACCOUNT;
-      document.getElementById("accountName").value = Weave.Identity.account;
-      document.getElementById("syncComputerName").value = Weave.Clients.localName;
+      document.getElementById("accountName").value = Weave.Service.identity.account;
+      document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP").hidden = this._usingCustomServer;
     }
   },
 
   startOver: function (showDialog) {
     if (showDialog) {
       let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
                   Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -10,17 +10,17 @@
 <!-- The page shown when logged in... -->
 
 <!-- Login error feedback -->
 <!ENTITY updatePass.label             "Update">
 <!ENTITY resetPass.label              "Reset">
 
 <!-- Manage Account -->
 <!ENTITY manageAccount.label          "Manage Account">
-<!ENTITY manageAccount.accesskey      "A">
+<!ENTITY manageAccount.accesskey      "n">
 <!ENTITY viewQuota.label              "View Quota">
 <!ENTITY changePassword2.label        "Change Password…">
 <!ENTITY myRecoveryKey.label          "My Recovery Key">
 <!ENTITY resetSync2.label             "Reset Sync…">
 
 <!ENTITY pairDevice.label             "Pair a Device">
 
 <!ENTITY syncMy.label               "Sync My">
--- a/dom/telephony/test/marionette/test_emergency.js
+++ b/dom/telephony/test/marionette/test_emergency.js
@@ -33,22 +33,28 @@ function dial() {
   is(outgoing.number, number);
   is(outgoing.state, "dialing");
 
   is(outgoing, telephony.active);
   //ok(telephony.calls === calls); // bug 717414
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoing);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + number + "        : unknown");
-    is(result[1], "OK");
-    answer();
-  });
+  outgoing.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoing, event.call);
+    is(outgoing.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + number + "        : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };
 }
 
 function answer() {
   log("Answering the emergency call.");
 
   // We get no "connecting" event when the remote party answers the call.
 
   outgoing.onconnected = function onconnected(event) {
--- a/dom/telephony/test/marionette/test_incoming_already_connected.js
+++ b/dom/telephony/test/marionette/test_incoming_already_connected.js
@@ -32,22 +32,28 @@ function dial() {
   ok(outgoingCall);
   is(outgoingCall.number, outNumber);
   is(outgoingCall.state, "dialing");
 
   is(outgoingCall, telephony.active);
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoingCall);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + outNumber + " : unknown");
-    is(result[1], "OK");
-    answer();
-  });
+  outgoingCall.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoingCall, event.call);
+    is(outgoingCall.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + outNumber + " : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };
 }
 
 function answer() {
   log("Answering the outgoing call.");
 
   // We get no "connecting" event when the remote party answers the call.
   outgoingCall.onconnected = function onconnectedOut(event) {
     log("Received 'connected' call event for the original outgoing call.");
--- a/dom/telephony/test/marionette/test_incoming_already_held.js
+++ b/dom/telephony/test/marionette/test_incoming_already_held.js
@@ -32,22 +32,28 @@ function dial() {
   ok(outgoingCall);
   is(outgoingCall.number, outNumber);
   is(outgoingCall.state, "dialing");
 
   is(outgoingCall, telephony.active);
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoingCall);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + outNumber + " : unknown");
-    is(result[1], "OK");
-    answer();
-  });
+  outgoingCall.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoingCall, event.call);
+    is(outgoingCall.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + outNumber + " : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };
 }
 
 function answer() {
   log("Answering the outgoing call.");
 
   // We get no "connecting" event when the remote party answers the call.
   outgoingCall.onconnected = function onconnectedOut(event) {
     log("Received 'connected' call event for the original outgoing call.");
--- a/dom/telephony/test/marionette/test_outgoing_already_held.js
+++ b/dom/telephony/test/marionette/test_outgoing_already_held.js
@@ -107,34 +107,40 @@ function holdCall(){
     });
   };
   incomingCall.hold();
 }
 
 // With one call on hold, make outgoing call
 function dial() {
   log("Making an outgoing call (while have one call already held).");
-  
+
   outgoingCall = telephony.dial(outNumber);
   ok(outgoingCall);
   is(outgoingCall.number, outNumber);
   is(outgoingCall.state, "dialing");
 
   is(outgoingCall, telephony.active);
   is(telephony.calls.length, 2);
   is(telephony.calls[0], incomingCall);
   is(telephony.calls[1], outgoingCall);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "inbound from " + inNumber + " : held");
-    is(result[1], "outbound to  " + outNumber + " : unknown");
-    is(result[2], "OK");
-    answerOutgoing();
-  });
+  outgoingCall.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoingCall, event.call);
+    is(outgoingCall.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "inbound from " + inNumber + " : held");
+      is(result[1], "outbound to  " + outNumber + " : ringing");
+      is(result[2], "OK");
+      answerOutgoing();
+    });
+  };
 }
 
 // Have the outgoing call answered
 function answerOutgoing() {
   log("Answering the outgoing/2nd call");
 
   // We get no "connecting" event when the remote party answers the call.
   outgoingCall.onconnected = function onconnectedOut(event) {
--- a/dom/telephony/test/marionette/test_outgoing_answer_hangup.js
+++ b/dom/telephony/test/marionette/test_outgoing_answer_hangup.js
@@ -33,22 +33,28 @@ function dial() {
   is(outgoing.number, number);
   is(outgoing.state, "dialing");
 
   is(outgoing, telephony.active);
   //ok(telephony.calls === calls); // bug 717414
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoing);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + number + " : unknown");
-    is(result[1], "OK");
-    answer();
-  });
+  outgoing.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoing, event.call);
+    is(outgoing.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + number + " : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };
 }
 
 function answer() {
   log("Answering the outgoing call.");
 
   // We get no "connecting" event when the remote party answers the call.
 
   outgoing.onconnected = function onconnected(event) {
--- a/dom/telephony/test/marionette/test_outgoing_hangup_held.js
+++ b/dom/telephony/test/marionette/test_outgoing_hangup_held.js
@@ -27,24 +27,33 @@ function verifyInitialState() {
 
 function dial() {
   log("Make an outgoing call.");
 
   outgoing = telephony.dial(number);
   ok(outgoing);
   is(outgoing.number, number);
   is(outgoing.state, "dialing");
-
   is(outgoing, telephony.active);
   //ok(telephony.calls === calls); // bug 717414
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoing);
 
-  // Get call list. Answer a call if the connection is established.
-  runEmulatorCmd("gsm list", cmdCallback);
+  outgoing.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoing, event.call);
+    is(outgoing.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + number + " : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };  
 }
 
 function answer() {
   log("Answering the outgoing call.");
 
   // We get no "connecting" event when the remote party answers the call.
 
   outgoing.onconnected = function onconnected(event) {
@@ -118,34 +127,14 @@ function hangUp() {
       is(result[0], "OK");
       cleanUp();
     });
   };
 
   outgoing.hangUp();
 }
 
-function cmdCallback(result) {
-  let unknownState = "outbound to  " + number + " : unknown";
-  let alertingState = "outbound to " + number + " : alerting";
-
-  log("Call list is now: " + result);
-
-  switch (result[0]) {
-    // Gsm list is empty. Wait until the connection is established.
-    case "OK":
-      runEmulatorCmd("gsm list", cmdCallback);
-      break;
-    // Answer the call now since the connection is established.
-    case unknownState: // Fall through ...
-    case alertingState:
-      is(result[1], "OK");
-      answer();
-      break;
-  }
-}
-
 function cleanUp() {
   SpecialPowers.removePermission("telephony", document);
   finish();
 }
 
 verifyInitialState();
--- a/dom/telephony/test/marionette/test_outgoing_hold_resume.js
+++ b/dom/telephony/test/marionette/test_outgoing_hold_resume.js
@@ -31,22 +31,28 @@ function dial() {
   ok(outgoingCall);
   is(outgoingCall.number, number);
   is(outgoingCall.state, "dialing");
 
   is(outgoingCall, telephony.active);
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoingCall);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + number + " : unknown");
-    is(result[1], "OK");
-    answer();
-  });
+  outgoingCall.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoingCall, event.call);
+    is(outgoingCall.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + number + " : ringing");
+      is(result[1], "OK");
+      answer();
+    });
+  };
 }
 
 function answer() {
   log("Answering the outgoing call.");
 
   // We get no "connecting" event when the remote party answers the call.
 
   outgoingCall.onconnected = function onconnected(event) {
--- a/dom/telephony/test/marionette/test_outgoing_reject.js
+++ b/dom/telephony/test/marionette/test_outgoing_reject.js
@@ -33,57 +33,49 @@ function dial() {
   is(outgoing.number, number);
   is(outgoing.state, "dialing");
 
   is(outgoing, telephony.active);
   //ok(telephony.calls === calls); // bug 717414
   is(telephony.calls.length, 1);
   is(telephony.calls[0], outgoing);
 
-  runEmulatorCmd("gsm list", function(result) {
-    log("Call list is now: " + result);
-    is(result[0], "outbound to  " + number + " : unknown");
-    is(result[1], "OK");
-    reject();
-  });
+  outgoing.onalerting = function onalerting(event) {
+    log("Received 'onalerting' call event.");
+    is(outgoing, event.call);
+    is(outgoing.state, "alerting");
+
+    runEmulatorCmd("gsm list", function(result) {
+      log("Call list is now: " + result);
+      is(result[0], "outbound to  " + number + " : ringing");
+      is(result[1], "OK");
+      reject();
+    });
+  };
 }
 
 function reject() {
   log("Reject the outgoing call on the other end.");
-
   // We get no "disconnecting" event when the remote party rejects the call.
 
   outgoing.ondisconnected = function ondisconnected(event) {
     log("Received 'disconnected' call event.");
     is(outgoing, event.call);
     is(outgoing.state, "disconnected");
 
     is(telephony.active, null);
     is(telephony.calls.length, 0);
 
     runEmulatorCmd("gsm list", function(result) {
       log("Call list is now: " + result);
       is(result[0], "OK");
       cleanUp();
     });
   };
-  runEmulatorCmd("gsm list", cmdCallback);
+  runEmulatorCmd("gsm cancel " + number);
 };
 
-function cmdCallback(result) {
-  let state = "outbound to  " + number + " : unknown";
-  log("Call list is now: " + result);
-
-  // The outgoing call cannot be canceled when call state is unknown.
-  // Wait until the call connection is established.
-  if (result[0] == state) {
-    runEmulatorCmd("gsm list", cmdCallback);
-  } else {
-    runEmulatorCmd("gsm cancel " + number);
-  }
-}
-
 function cleanUp() {
   SpecialPowers.removePermission("telephony", document);
   finish();
 }
 
 verifyInitialState();
--- a/editor/libeditor/base/nsEditorEventListener.cpp
+++ b/editor/libeditor/base/nsEditorEventListener.cpp
@@ -279,16 +279,17 @@ NS_IMPL_ISUPPORTS1(nsEditorEventListener
 /**
  *  nsIDOMEventListener implementation
  */
 
 NS_IMETHODIMP
 nsEditorEventListener::HandleEvent(nsIDOMEvent* aEvent)
 {
   NS_ENSURE_TRUE(mEditor, NS_ERROR_NOT_AVAILABLE);
+  nsCOMPtr<nsIEditor> kungFuDeathGrip = mEditor;
 
   nsAutoString eventType;
   aEvent->GetType(eventType);
 
   nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
   if (dragEvent) {
     if (eventType.EqualsLiteral("dragenter"))
       return DragEnter(dragEvent);
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -3909,18 +3909,17 @@ nsCSSFrameConstructor::GetAnonymousConte
   uint32_t count = aContent.Length();
   for (uint32_t i=0; i < count; i++) {
     // get our child's content and set its parent to our content
     nsIContent* content = aContent[i].mContent;
     NS_ASSERTION(content, "null anonymous content?");
 
     // least-surprise CSS binding until we do the SVG specified
     // cascading rules for <svg:use> - bug 265894
-    if (aParent &&
-        aParent->NodeInfo()->Equals(nsGkAtoms::use, kNameSpaceID_SVG)) {
+    if (aParentFrame->GetType() == nsGkAtoms::svgUseFrame) {
       content->SetFlags(NODE_IS_ANONYMOUS);
     } else {
       content->SetNativeAnonymous();
     }
 
     bool anonContentIsEditable = content->HasFlag(NODE_IS_EDITABLE);
     rv = content->BindToTree(mDocument, aParent, aParent, true);
     // If the anonymous content creator requested that the content should be
--- a/services/common/Makefile.in
+++ b/services/common/Makefile.in
@@ -18,16 +18,17 @@ modules := \
   storageservice.js \
   stringbundle.js \
   tokenserverclient.js \
   utils.js \
   $(NULL)
 
 testing_modules := \
   aitcserver.js \
+  logging.js \
   storageserver.js \
   utils.js \
   $(NULL)
 
 TEST_DIRS += tests
 
 PREF_JS_EXPORTS = $(srcdir)/services-common.js
 
new file mode 100644
--- /dev/null
+++ b/services/common/modules-testing/logging.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "getTestLogger",
+  "initTestLogging",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+
+function initTestLogging(level) {
+  function LogStats() {
+    this.errorsLogged = 0;
+  }
+  LogStats.prototype = {
+    format: function 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();
+
+  let log = Log4Moz.repository.rootLogger;
+  let logStats = new LogStats();
+  let appender = new Log4Moz.DumpAppender(logStats);
+
+  if (typeof(level) == "undefined") {
+    level = "Debug";
+  }
+  getTestLogger().level = Log4Moz.Level[level];
+  Log4Moz.repository.getLogger("Services").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");
+}
+
--- a/services/common/modules-testing/storageserver.js
+++ b/services/common/modules-testing/storageserver.js
@@ -244,16 +244,17 @@ ServerBSO.prototype = {
         default:
           this._log.warn("Unexpected field in BSO record: " + key);
           sendMozSvcError(request, response, "8");
           return true;
       }
     }
 
     this.modified = request.timestamp;
+    this.deleted = false;
     response.setHeader("X-Last-Modified", "" + this.modified, false);
 
     response.setStatusLine(request.httpVersion, code, status);
   },
 };
 
 /**
  * Represent a collection on the server.
@@ -545,16 +546,17 @@ StorageServerCollection.prototype = {
           this._log.debug("Creating BSO " + JSON.stringify(record.id) +
                           " on the fly.");
           bso = new ServerBSO(record.id);
           this.insertBSO(bso);
         }
         if (bso) {
           bso.payload = record.payload;
           bso.modified = timestamp;
+          bso.deleted = false;
           success.push(record.id);
 
           if (record.sortindex) {
             bso.sortindex = parseInt(record.sortindex, 10);
           }
 
         } else {
           failed[record.id] = "no bso configured";
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -1,15 +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/. */
 
-Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/services-common/logging.js");
 
 let btoa = Cu.import("resource://services-common/log4moz.js").btoa;
 let atob = Cu.import("resource://services-common/log4moz.js").atob;
 
 function do_check_empty(obj) {
   do_check_attribute_count(obj, 0);
 }
 
@@ -40,53 +41,16 @@ function do_check_throws(aFunc, aResult,
  *
  * @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();
-
-  let log = Log4Moz.repository.rootLogger;
-  let logStats = new LogStats();
-  let appender = new Log4Moz.DumpAppender(logStats);
-
-  if (typeof(level) == "undefined") {
-    level = "Debug";
-  }
-  getTestLogger().level = Log4Moz.Level[level];
-  Log4Moz.repository.getLogger("Services").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");
-}
-
 /**
  * Obtain a port number to run a server on.
  *
  * In the ideal world, this would be dynamic so multiple servers could be run
  * in parallel.
  */
 function get_server_port() {
   return 8080;
--- a/services/common/tests/unit/test_load_modules.js
+++ b/services/common/tests/unit/test_load_modules.js
@@ -9,16 +9,17 @@ const modules = [
   "storageservice.js",
   "stringbundle.js",
   "tokenserverclient.js",
   "utils.js",
 ];
 
 const test_modules = [
   "aitcserver.js",
+  "logging.js",
   "storageserver.js",
 ];
 
 function run_test() {
   for each (let m in modules) {
     let resource = "resource://services-common/" + m;
     Components.utils.import(resource, {});
   }
--- a/services/common/tests/unit/test_storage_server.js
+++ b/services/common/tests/unit/test_storage_server.js
@@ -587,8 +587,94 @@ add_test(function test_x_num_records() {
     let col = localRequest("/2.0/123/storage/crypto");
     col.get(function (err) {
       // Collection fetches do.
       do_check_eq(this.response.headers["x-num-records"], "2");
       server.stop(run_next_test);
     });
   });
 });
+
+add_test(function test_put_delete_put() {
+  _("Bug 790397: Ensure BSO deleted flag is reset on PUT.");
+
+  let server = new StorageServer();
+  server.registerUser("123", "password");
+  server.createContents("123", {
+    test: {bso: {foo: "bar"}}
+  });
+  server.startSynchronous(PORT);
+
+  _("Ensure we can PUT an existing record.");
+  let request1 = localRequest("/2.0/123/storage/test/bso", "123", "password");
+  request1.setHeader("Content-Type", "application/json");
+  let payload1 = JSON.stringify({"payload": "foobar"});
+  let error1 = doPutRequest(request1, payload1);
+  do_check_eq(null, error1);
+  do_check_eq(request1.response.status, 204);
+
+  _("Ensure we can DELETE it.");
+  let request2 = localRequest("/2.0/123/storage/test/bso", "123", "password");
+  let error2 = doDeleteRequest(request2);
+  do_check_eq(error2, null);
+  do_check_eq(request2.response.status, 204);
+  do_check_false("content-type" in request2.response.headers);
+
+  _("Ensure we can PUT a previously deleted record.");
+  let request3 = localRequest("/2.0/123/storage/test/bso", "123", "password");
+  request3.setHeader("Content-Type", "application/json");
+  let payload3 = JSON.stringify({"payload": "foobar"});
+  let error3 = doPutRequest(request3, payload3);
+  do_check_eq(null, error3);
+  do_check_eq(request3.response.status, 201);
+
+  _("Ensure we can GET the re-uploaded record.");
+  let request4 = localRequest("/2.0/123/storage/test/bso", "123", "password");
+  let error4 = doGetRequest(request4);
+  do_check_eq(error4, null);
+  do_check_eq(request4.response.status, 200);
+  do_check_eq(request4.response.headers["content-type"], "application/json");
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_collection_get_newer() {
+  _("Ensure get with newer argument on collection works.");
+
+  let server = new StorageServer();
+  server.registerUser("123", "password");
+  server.startSynchronous(PORT);
+
+  let coll = server.user("123").createCollection("test");
+  let bso1 = coll.insert("001", {foo: "bar"});
+  let bso2 = coll.insert("002", {bar: "foo"});
+
+  // Don't want both records to have the same timestamp.
+  bso2.modified = bso1.modified + 1000;
+
+  function newerRequest(newer) {
+    return localRequest("/2.0/123/storage/test?newer=" + newer,
+                        "123", "password");
+  }
+
+  let request1 = newerRequest(0);
+  let error1 = doGetRequest(request1);
+  do_check_null(error1);
+  do_check_eq(request1.response.status, 200);
+  let items1 = JSON.parse(request1.response.body).items;
+  do_check_attribute_count(items1, 2);
+
+  let request2 = newerRequest(bso1.modified + 1);
+  let error2 = doGetRequest(request2);
+  do_check_null(error2);
+  do_check_eq(request2.response.status, 200);
+  let items2 = JSON.parse(request2.response.body).items;
+  do_check_attribute_count(items2, 1);
+
+  let request3 = newerRequest(bso2.modified + 1);
+  let error3 = doGetRequest(request3);
+  do_check_null(error3);
+  do_check_eq(request3.response.status, 200);
+  let items3 = JSON.parse(request3.response.body).items;
+  do_check_attribute_count(items3, 0);
+
+  server.stop(run_next_test);
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_utils_ensureMillisecondsTimestamp.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/utils.js");
+
+function run_test() {
+    do_check_null(CommonUtils.ensureMillisecondsTimestamp(null));
+    do_check_null(CommonUtils.ensureMillisecondsTimestamp(0));
+    do_check_null(CommonUtils.ensureMillisecondsTimestamp("0"));
+    do_check_null(CommonUtils.ensureMillisecondsTimestamp("000"));
+
+    do_check_null(CommonUtils.ensureMillisecondsTimestamp(999 * 10000000000));
+
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp(-1); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp(1); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp(1.5); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp(999 * 10000000000 + 0.5); });
+
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp("-1"); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp("1"); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp("1.5"); });
+    do_check_throws(function err() { CommonUtils.ensureMillisecondsTimestamp("" + (999 * 10000000000 + 0.5)); });
+}
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -4,16 +4,17 @@ tail =
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
 
 [test_utils_atob.js]
 [test_utils_deepCopy.js]
 [test_utils_encodeBase32.js]
 [test_utils_encodeBase64URL.js]
+[test_utils_ensureMillisecondsTimestamp.js]
 [test_utils_json.js]
 [test_utils_makeURI.js]
 [test_utils_namedTimer.js]
 [test_utils_stackTrace.js]
 [test_utils_utf8.js]
 
 [test_aitc_server.js]
 [test_async_chain.js]
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -426,27 +426,29 @@ let CommonUtils = {
    * @param value
    *        Value to validate.
    */
   ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
     if (!value) {
       return;
     }
 
-    if (value < 0) {
-      throw new Error("Timestamp value is negative: " + value);
+    if (!/^[0-9]+$/.test(value)) {
+      throw new Error("Timestamp value is not a positive integer: " + value);
+    }
+
+    let intValue = parseInt(value, 10);
+
+    if (!intValue) {
+       return;
     }
 
     // Catch what looks like seconds, not milliseconds.
-    if (value < 10000000000) {
-      throw new Error("Timestamp appears to be in seconds: " + value);
-    }
-
-    if (Math.floor(value) != Math.ceil(value)) {
-      throw new Error("Timestamp value is not an integer: " + value);
+    if (intValue < 10000000000) {
+      throw new Error("Timestamp appears to be in seconds: " + intValue);
     }
   },
 
   /**
    * Read bytes from an nsIInputStream into a string.
    *
    * @param stream
    *        (nsIInputStream) Stream to read from.
--- a/services/sync/Makefile.in
+++ b/services/sync/Makefile.in
@@ -34,16 +34,17 @@ sync_modules := \
   main.js \
   notifications.js \
   policies.js \
   record.js \
   resource.js \
   rest.js \
   service.js \
   status.js \
+  userapi.js \
   util.js \
   $(NULL)
 
 # The set of JavaScript modules provide engines for Sync. These are
 # copied as-is.
 sync_engine_modules := \
   addons.js \
   apps.js \
@@ -51,16 +52,27 @@ sync_engine_modules := \
   clients.js \
   forms.js \
   history.js \
   passwords.js \
   prefs.js \
   tabs.js \
   $(NULL)
 
+sync_stage_modules := \
+  cluster.js \
+  enginesync.js \
+  $(NULL)
+
+sync_testing_modules := \
+  fakeservices.js \
+  rotaryengine.js \
+  utils.js \
+  $(NULL)
+
 DIRS += locales
 TEST_DIRS += tests
 
 EXTRA_COMPONENTS := \
   SyncComponents.manifest \
   Weave.js \
   $(NULL)
 
@@ -70,9 +82,16 @@ PREF_JS_EXPORTS := $(srcdir)/services-sy
 SYNC_MAIN_FILES := $(addprefix modules/,$(sync_modules))
 SYNC_MAIN_DEST = $(FINAL_TARGET)/modules/services-sync
 INSTALL_TARGETS += SYNC_MAIN
 
 SYNC_ENGINES_FILES := $(addprefix modules/engines/,$(sync_engine_modules))
 SYNC_ENGINES_DEST = $(FINAL_TARGET)/modules/services-sync/engines
 INSTALL_TARGETS += SYNC_ENGINES
 
+SYNC_STAGES_FILES := $(addprefix modules/stages/,$(sync_stage_modules))
+SYNC_STAGES_DEST = $(FINAL_TARGET)/modules/services-sync/stages
+INSTALL_TARGETS += SYNC_STAGES
+
+TESTING_JS_MODULES := $(addprefix modules-testing/,$(sync_testing_modules))
+TESTING_JS_MODULE_DIR := services/sync
+
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/sync/modules-testing/fakeservices.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "FakeCryptoService",
+  "FakeFilesystemService",
+  "FakeGUIDService",
+  "fakeSHA256HMAC",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+
+let btoa = Cu.import("resource://services-common/log4moz.js").btoa;
+
+function FakeFilesystemService(contents) {
+  this.fakeContents = contents;
+  let self = this;
+
+  Utils.jsonSave = function jsonSave(filePath, that, obj, callback) {
+    let json = typeof obj == "function" ? obj.call(that) : obj;
+    self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
+    callback.call(that);
+  };
+
+  Utils.jsonLoad = function jsonLoad(filePath, that, cb) {
+    let obj;
+    let json = self.fakeContents["weave/" + filePath + ".json"];
+    if (json) {
+      obj = JSON.parse(json);
+    }
+    cb.call(that, obj);
+  };
+};
+
+function fakeSHA256HMAC(message) {
+   message = message.substr(0, 64);
+   while (message.length < 64) {
+     message += " ";
+   }
+   return message;
+}
+
+function FakeGUIDService() {
+  let latestGUID = 0;
+
+  Utils.makeGUID = function makeGUID() {
+    return "fake-guid-" + latestGUID++;
+  };
+}
+
+/*
+ * Mock implementation of WeaveCrypto. It does not encrypt or
+ * decrypt, merely returning the input verbatim.
+ */
+function FakeCryptoService() {
+  this.counter = 0;
+
+  delete Svc.Crypto;  // get rid of the getter first
+  Svc.Crypto = this;
+
+  CryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC(keyBundle) {
+    return fakeSHA256HMAC(this.ciphertext);
+  };
+}
+FakeCryptoService.prototype = {
+
+  encrypt: function encrypt(clearText, symmetricKey, iv) {
+    return clearText;
+  },
+
+  decrypt: function decrypt(cipherText, symmetricKey, iv) {
+    return cipherText;
+  },
+
+  generateRandomKey: function generateRandomKey() {
+    return btoa("fake-symmetric-key-" + this.counter++);
+  },
+
+  generateRandomIV: function generateRandomIV() {
+    // A base64-encoded IV is 24 characters long
+    return btoa("fake-fake-fake-random-iv");
+  },
+
+  expandData: function expandData(data, len) {
+    return data;
+  },
+
+  deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase,
+                                                            salt, keyLength) {
+    return "some derived key string composed of bytes";
+  },
+
+  generateRandomBytes: function generateRandomBytes(byteCount) {
+    return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(byteCount);
+  }
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/modules-testing/rotaryengine.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "RotaryEngine",
+  "RotaryRecord",
+  "RotaryStore",
+  "RotaryTracker",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+
+/*
+ * A fake engine implementation.
+ * This is used all over the place.
+ *
+ * Complete with record, store, and tracker implementations.
+ */
+
+function RotaryRecord(collection, id) {
+  CryptoWrapper.call(this, collection, id);
+}
+RotaryRecord.prototype = {
+  __proto__: CryptoWrapper.prototype
+};
+Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]);
+
+function RotaryStore(engine) {
+  Store.call(this, "Rotary", engine);
+  this.items = {};
+}
+RotaryStore.prototype = {
+  __proto__: Store.prototype,
+
+  create: function create(record) {
+    this.items[record.id] = record.denomination;
+  },
+
+  remove: function remove(record) {
+    delete this.items[record.id];
+  },
+
+  update: function update(record) {
+    this.items[record.id] = record.denomination;
+  },
+
+  itemExists: function itemExists(id) {
+    return (id in this.items);
+  },
+
+  createRecord: function createRecord(id, collection) {
+    let record = new RotaryRecord(collection, id);
+
+    if (!(id in this.items)) {
+      record.deleted = true;
+      return record;
+    }
+
+    record.denomination = this.items[id] || "Data for new record: " + id;
+    return record;
+  },
+
+  changeItemID: function changeItemID(oldID, newID) {
+    if (oldID in this.items) {
+      this.items[newID] = this.items[oldID];
+    }
+
+    delete this.items[oldID];
+  },
+
+  getAllIDs: function getAllIDs() {
+    let ids = {};
+    for (let id in this.items) {
+      ids[id] = true;
+    }
+    return ids;
+  },
+
+  wipe: function wipe() {
+    this.items = {};
+  }
+};
+
+function RotaryTracker(engine) {
+  Tracker.call(this, "Rotary", engine);
+}
+RotaryTracker.prototype = {
+  __proto__: Tracker.prototype
+};
+
+
+function RotaryEngine(service) {
+  SyncEngine.call(this, "Rotary", service);
+  // Ensure that the engine starts with a clean slate.
+  this.toFetch        = [];
+  this.previousFailed = [];
+}
+RotaryEngine.prototype = {
+  __proto__: SyncEngine.prototype,
+  _storeObj: RotaryStore,
+  _trackerObj: RotaryTracker,
+  _recordObj: RotaryRecord,
+
+  _findDupe: function _findDupe(item) {
+    // This is a semaphore used for testing proper reconciling on dupe
+    // detection.
+    if (item.id == "DUPE_INCOMING") {
+      return "DUPE_LOCAL";
+    }
+
+    for (let [id, value] in Iterator(this._store.items)) {
+      if (item.denomination == value) {
+        return id;
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/modules-testing/utils.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "TEST_CLUSTER_URL",
+  "TEST_SERVER_URL",
+  "btoa", // It comes from a module import.
+  "encryptPayload",
+  "setBasicCredentials",
+  "SyncTestingInfrastructure",
+  "waitForZeroTimer",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://testing-common/services-common/logging.js");
+Cu.import("resource://testing-common/services/sync/fakeservices.js");
+
+const TEST_SERVER_URL = "http://localhost:8080/";
+const TEST_CLUSTER_URL = TEST_SERVER_URL;
+
+/**
+ * First wait >100ms (nsITimers can take up to that much time to fire, so
+ * we can account for the timer in delayedAutoconnect) and then two event
+ * loop ticks (to account for the Utils.nextTick() in autoConnect).
+ */
+function waitForZeroTimer(callback) {
+  let ticks = 2;
+  function wait() {
+    if (ticks) {
+      ticks -= 1;
+      CommonUtils.nextTick(wait);
+      return;
+    }
+    callback();
+  }
+  CommonUtils.namedTimer(wait, 150, {}, "timer");
+}
+
+function setBasicCredentials(username, password, syncKey) {
+  let ns = {};
+  Cu.import("resource://services-sync/service.js", ns);
+
+  let auth = ns.Service.identity;
+  auth.username = username;
+  auth.basicPassword = password;
+  auth.syncKey = syncKey;
+}
+
+function SyncTestingInfrastructure(username, password, syncKey) {
+  let ns = {};
+  Cu.import("resource://services-sync/service.js", ns);
+
+  let auth = ns.Service.identity;
+  auth.account = username || "foo";
+  auth.basicPassword = password || "password";
+  auth.syncKey = syncKey || "foo";
+
+  ns.Service.serverURL = TEST_SERVER_URL;
+  ns.Service.clusterURL = TEST_CLUSTER_URL;
+
+  this.logStats = initTestLogging();
+  this.fakeFilesystem = new FakeFilesystemService({});
+  this.fakeGUIDService = new FakeGUIDService();
+  this.fakeCryptoService = new FakeCryptoService();
+}
+
+/**
+ * Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
+ */
+function encryptPayload(cleartext) {
+  if (typeof cleartext == "object") {
+    cleartext = JSON.stringify(cleartext);
+  }
+
+  return {
+    ciphertext: cleartext, // ciphertext == cleartext with fake crypto
+    IV: "irrelevant",
+    hmac: fakeSHA256HMAC(cleartext, CryptoUtils.makeHMACKey("")),
+  };
+}
+
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -15,17 +15,16 @@
  * hopefully ported.
  */
 
 "use strict";
 
 const Cu = Components.utils;
 
 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;
 const CHANGE_ENABLED     = 3;
@@ -114,18 +113,16 @@ const EXPORTED_SYMBOLS = ["AddonsReconci
  * events will occur immediately. However, we still see disabling events and
  * heed them like they were normal. In the end, the state is proper.
  */
 function AddonsReconciler() {
   this._log = Log4Moz.repository.getLogger("Sync.AddonsReconciler");
   let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
   this._log.level = Log4Moz.Level[level];
 
-  Svc.Obs.add("weave:engine:start-tracking", this.startListening, this);
-  Svc.Obs.add("weave:engine:stop-tracking", this.stopListening, this);
   Svc.Obs.add("xpcom-shutdown", this.stopListening, this);
 };
 AddonsReconciler.prototype = {
   /** Flag indicating whether we are listening to AddonManager events. */
   _listening: false,
 
   /** Whether state has been loaded from a file.
    *
@@ -296,18 +293,17 @@ AddonsReconciler.prototype = {
   },
 
   /**
    * Tells the instance to start listening for AddonManager changes.
    *
    * This is typically called automatically when Sync is loaded.
    */
   startListening: function startListening() {
-    let engine = Engines.get("addons");
-    if (!engine || !engine.enabled || this._listening) {
+    if (this._listening) {
       return;
     }
 
     this._log.info("Registering as Add-on Manager listener.");
     AddonManager.addAddonListener(this);
     AddonManager.addInstallListener(this);
     this._listening = true;
   },
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -174,11 +174,13 @@ kFirstSyncChoiceNotMade:               "
 
 // Application IDs
 FIREFOX_ID:                            "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
 FENNEC_ID:                             "{a23983c0-fd0e-11dc-95ff-0800200c9a66}",
 SEAMONKEY_ID:                          "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}",
 TEST_HARNESS_ID:                       "xuth@mozilla.org",
 
 MIN_PP_LENGTH:                         12,
-MIN_PASS_LENGTH:                       8
+MIN_PASS_LENGTH:                       8,
+
+LOG_DATE_FORMAT:                       "%Y-%m-%d %H:%M:%S",
 
 }))];
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -1,45 +1,51 @@
 /* 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 = ['Engines', 'Engine', 'SyncEngine',
-                          'Tracker', 'Store'];
+const EXPORTED_SYMBOLS = [
+  "EngineManager",
+  "Engine",
+  "SyncEngine",
+  "Tracker",
+  "Store"
+];
 
-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-common/async.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-common/log4moz.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");    // 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.
  *
  * There are two things they keep track of:
  * 1) A score, indicating how urgently the engine wants to sync
  * 2) A list of IDs for all the changed items that need to be synced
  * and updating their 'score', indicating how urgently they
  * want to sync.
  *
  */
-function Tracker(name) {
+function Tracker(name, engine) {
+  if (!engine) {
+    throw new Error("Tracker must be associated with an Engine instance.");
+  }
+
   name = name || "Unnamed";
   this.name = this.file = name.toLowerCase();
+  this.engine = engine;
 
   this._log = Log4Moz.repository.getLogger("Sync.Tracker." + name);
   let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
   this._log.level = Log4Moz.Level[level];
 
   this._score = 0;
   this._ignored = [];
   this.ignoreAll = false;
@@ -160,19 +166,24 @@ Tracker.prototype = {
  * Store implementations require a number of functions to be implemented. These
  * are all documented below.
  *
  * For stores that deal with many records or which have expensive store access
  * routines, it is highly recommended to implement a custom applyIncomingBatch
  * and/or applyIncoming function on top of the basic APIs.
  */
 
-function Store(name) {
+function Store(name, engine) {
+  if (!engine) {
+    throw new Error("Store must be associated with an Engine instance.");
+  }
+
   name = name || "Unnamed";
   this.name = name.toLowerCase();
+  this.engine = engine;
 
   this._log = Log4Moz.repository.getLogger("Sync.Store." + name);
   let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
   this._log.level = Log4Moz.Level[level];
 
   XPCOMUtils.defineLazyGetter(this, "_timer", function() {
     return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   });
@@ -346,31 +357,26 @@ Store.prototype = {
    * can be thought of as clearing out all state and restoring the "new
    * browser" state.
    */
   wipe: function Store_wipe() {
     throw "override wipe in a subclass";
   }
 };
 
-
-// Singleton service, holds registered engines
+function EngineManager(service) {
+  this.service = service;
 
-XPCOMUtils.defineLazyGetter(this, "Engines", function() {
-  return new EngineManagerSvc();
-});
-
-function EngineManagerSvc() {
   this._engines = {};
   this._log = Log4Moz.repository.getLogger("Sync.EngineManager");
   this._log.level = Log4Moz.Level[Svc.Prefs.get(
     "log.logger.service.engines", "Debug")];
 }
-EngineManagerSvc.prototype = {
-  get: function EngMgr_get(name) {
+EngineManager.prototype = {
+  get: function get(name) {
     // Return an array of engines if we have an array of names
     if (Array.isArray(name)) {
       let engines = [];
       name.forEach(function(name) {
         let engine = this.get(name);
         if (engine)
           engines.push(engine);
       }, this);
@@ -380,66 +386,82 @@ EngineManagerSvc.prototype = {
     let engine = this._engines[name];
     if (!engine) {
       this._log.debug("Could not get engine: " + name);
       if (Object.keys)
         this._log.debug("Engines are: " + JSON.stringify(Object.keys(this._engines)));
     }
     return engine;
   },
-  getAll: function EngMgr_getAll() {
-    return [engine for ([name, engine] in Iterator(Engines._engines))];
+
+  getAll: function getAll() {
+    return [engine for ([name, engine] in Iterator(this._engines))];
   },
-  getEnabled: function EngMgr_getEnabled() {
+
+  getEnabled: function getEnabled() {
     return this.getAll().filter(function(engine) engine.enabled);
   },
-  
+
   /**
    * Register an Engine to the service. Alternatively, give an array of engine
    * objects to register.
    *
    * @param engineObject
    *        Engine object used to get an instance of the engine
    * @return The engine object if anything failed
    */
-  register: function EngMgr_register(engineObject) {
+  register: function register(engineObject) {
     if (Array.isArray(engineObject))
       return engineObject.map(this.register, this);
 
     try {
-      let engine = new engineObject();
+      let engine = new engineObject(this.service);
       let name = engine.name;
       if (name in this._engines)
         this._log.error("Engine '" + name + "' is already registered!");
       else
         this._engines[name] = engine;
     }
     catch(ex) {
+      this._log.error(CommonUtils.exceptionStr(ex));
+
       let mesg = ex.message ? ex.message : ex;
       let name = engineObject || "";
       name = name.prototype || "";
       name = name.name || "";
 
       let out = "Could not initialize engine '" + name + "': " + mesg;
       this._log.error(out);
 
       return engineObject;
     }
   },
-  unregister: function EngMgr_unregister(val) {
+
+  unregister: function unregister(val) {
     let name = val;
     if (val instanceof Engine)
       name = val.name;
     delete this._engines[name];
-  }
+  },
+
+  clear: function clear() {
+    for (let name in this._engines) {
+      delete this._engines[name];
+    }
+  },
 };
 
-function Engine(name) {
+function Engine(name, service) {
+  if (!service) {
+    throw new Error("Engine must be associated with a Service instance.");
+  }
+
   this.Name = name || "Unnamed";
   this.name = name.toLowerCase();
+  this.service = service;
 
   this._notify = Utils.notify("weave:engine:");
   this._log = Log4Moz.repository.getLogger("Sync.Engine." + this.Name);
   let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
   this._log.level = Log4Moz.Level[level];
 
   this._tracker; // initialize tracker to load previously changed IDs
   this._log.debug("Engine initialized");
@@ -455,23 +477,23 @@ Engine.prototype = {
 
   get prefName() this.name,
   get enabled() Svc.Prefs.get("engine." + this.prefName, false),
   set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val),
 
   get score() this._tracker.score,
 
   get _store() {
-    let store = new this._storeObj(this.Name);
+    let store = new this._storeObj(this.Name, this);
     this.__defineGetter__("_store", function() store);
     return store;
   },
 
   get _tracker() {
-    let tracker = new this._trackerObj(this.Name);
+    let tracker = new this._trackerObj(this.Name, this);
     this.__defineGetter__("_tracker", function() tracker);
     return tracker;
   },
 
   sync: function Engine_sync() {
     if (!this.enabled)
       return;
 
@@ -500,18 +522,19 @@ Engine.prototype = {
     this._tracker.clearChangedIDs();
   },
 
   wipeClient: function Engine_wipeClient() {
     this._notify("wipe-client", this.name, this._wipeClient)();
   }
 };
 
-function SyncEngine(name) {
-  Engine.call(this, name || "SyncEngine");
+function SyncEngine(name, service) {
+  Engine.call(this, name || "SyncEngine", service);
+
   this.loadToFetch();
   this.loadPreviousFailed();
 }
 
 // Enumeration to define approaches to handling bad records.
 // Attached to the constructor to allow use as a kind of static enumeration.
 SyncEngine.kRecoveryStrategy = {
   ignore: "ignore",
@@ -532,17 +555,17 @@ SyncEngine.prototype = {
   // URI length limitations.
   guidFetchBatchSize: DEFAULT_GUID_FETCH_BATCH_SIZE,
   mobileGUIDFetchBatchSize: DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE,
   
   // How many records to process in a single batch.
   applyIncomingBatchSize: DEFAULT_STORE_BATCH_SIZE,
 
   get storageURL() Svc.Prefs.get("clusterURL") + SYNC_API_VERSION +
-    "/" + Identity.username + "/storage/",
+    "/" + this.service.identity.username + "/storage/",
 
   get engineURL() this.storageURL + this.name,
 
   get cryptoKeysURL() this.storageURL + "crypto/keys",
 
   get metaURL() this.storageURL + "meta/global",
 
   get syncID() {
@@ -644,17 +667,17 @@ SyncEngine.prototype = {
     record.collection = this.name;
     return record;
   },
 
   // Any setup that needs to happen at the beginning of each sync.
   _syncStartup: function SyncEngine__syncStartup() {
 
     // Determine if we need to wipe on outdated versions
-    let metaGlobal = Records.get(this.metaURL);
+    let metaGlobal = this.service.recordManager.get(this.metaURL);
     let engines = metaGlobal.payload.engines || {};
     let engineData = engines[this.name] || {};
 
     let needsWipe = false;
 
     // Assume missing versions are 0 and wipe the server
     if ((engineData.version || 0) < this.version) {
       this._log.debug("Old engine data: " + [engineData.version, this.version]);
@@ -720,17 +743,17 @@ SyncEngine.prototype = {
   },
 
   // Process incoming records
   _processIncoming: function SyncEngine__processIncoming() {
     this._log.trace("Downloading & applying server changes");
 
     // Figure out how many total items to fetch this sync; do less on mobile.
     let batchSize = Infinity;
-    let newitems = new Collection(this.engineURL, this._recordObj);
+    let newitems = new Collection(this.engineURL, this._recordObj, this.service);
     let isMobile = (Svc.Prefs.get("client.type") == "mobile");
 
     if (isMobile) {
       batchSize = MOBILE_BATCH_SIZE;
     }
     newitems.newer = this.lastSync;
     newitems.full  = true;
     newitems.limit = batchSize;
@@ -777,51 +800,55 @@ SyncEngine.prototype = {
       if (failed.length) {
         this.previousFailed = Utils.arrayUnion(failed, this.previousFailed);
         count.failed += failed.length;
         this._log.debug("Records that failed to apply: " + failed);
         failed = [];
       }
     }
 
+    let key = this.service.collectionKeys.keyForCollection(this.name);
+
     // Not binding this method to 'this' for performance reasons. It gets
     // called for every incoming record.
     let self = this;
+
     newitems.recordHandler = function(item) {
       if (aborting) {
         return;
       }
 
       // Grab a later last modified if possible
       if (self.lastModified == null || item.modified > self.lastModified)
         self.lastModified = item.modified;
 
       // Track the collection for the WBO.
       item.collection = self.name;
-      
+
       // Remember which records were processed
       handled.push(item.id);
 
       try {
         try {
-          item.decrypt();
+          item.decrypt(key);
         } catch (ex if Utils.isHMACMismatch(ex)) {
           let strategy = self.handleHMACMismatch(item, true);
           if (strategy == SyncEngine.kRecoveryStrategy.retry) {
             // You only get one retry.
             try {
               // Try decrypting again, typically because we've got new keys.
               self._log.info("Trying decrypt again...");
-              item.decrypt();
+              key = self.service.collectionKeys.keyForCollection(self.name);
+              item.decrypt(key);
               strategy = null;
             } catch (ex if Utils.isHMACMismatch(ex)) {
               strategy = self.handleHMACMismatch(item, false);
             }
           }
-          
+
           switch (strategy) {
             case null:
               // Retry succeeded! No further handling.
               break;
             case SyncEngine.kRecoveryStrategy.retry:
               self._log.debug("Ignoring second retry suggestion.");
               // Fall through to error case.
             case SyncEngine.kRecoveryStrategy.error:
@@ -879,18 +906,18 @@ SyncEngine.prototype = {
 
       if (aborting) {
         throw aborting;
       }
     }
 
     // Mobile: check if we got the maximum that we requested; get the rest if so.
     if (handled.length == newitems.limit) {
-      let guidColl = new Collection(this.engineURL);
-      
+      let guidColl = new Collection(this.engineURL, null, this.service);
+
       // Sort and limit so that on mobile we only get the last X records.
       guidColl.limit = this.downloadLimit;
       guidColl.newer = this.lastSync;
 
       // index: Orders by the sortindex descending (highest weight first).
       guidColl.sort  = "index";
 
       let guids = guidColl.get();
@@ -1173,17 +1200,17 @@ SyncEngine.prototype = {
     this._log.trace("Uploading local changes to server.");
 
     let modifiedIDs = Object.keys(this._modified);
     if (modifiedIDs.length) {
       this._log.trace("Preparing " + modifiedIDs.length +
                       " outgoing records");
 
       // collection we'll upload
-      let up = new Collection(this.engineURL);
+      let up = new Collection(this.engineURL, null, this.service);
       let count = 0;
 
       // Upload what we've got so far in the collection
       let doUpload = Utils.bind2(this, function(desc) {
         this._log.info("Uploading " + desc + " of " + modifiedIDs.length +
                        " records");
         let resp = up.post();
         if (!resp.success) {
@@ -1212,17 +1239,17 @@ SyncEngine.prototype = {
       });
 
       for each (let id in modifiedIDs) {
         try {
           let out = this._createRecord(id);
           if (this._log.level <= Log4Moz.Level.Trace)
             this._log.trace("Outgoing: " + out);
 
-          out.encrypt();
+          out.encrypt(this.service.collectionKeys.keyForCollection(this.name));
           up.pushData(out);
         }
         catch(ex) {
           this._log.warn("Error creating record: " + Utils.exceptionStr(ex));
         }
 
         // Partial upload
         if ((++count % MAX_UPLOAD_RECORDS) == 0)
@@ -1239,17 +1266,17 @@ SyncEngine.prototype = {
 
   // Any cleanup necessary.
   // Save the current snapshot so as to calculate changes at next sync
   _syncFinish: function SyncEngine__syncFinish() {
     this._log.trace("Finishing up sync");
     this._tracker.resetScore();
 
     let doDelete = Utils.bind2(this, function(key, val) {
-      let coll = new Collection(this.engineURL, this._recordObj);
+      let coll = new Collection(this.engineURL, this._recordObj, this.service);
       coll[key] = val;
       coll.delete();
     });
 
     for (let [key, val] in Iterator(this._delete)) {
       // Remove the key for future uses
       delete this._delete[key];
 
@@ -1290,24 +1317,26 @@ SyncEngine.prototype = {
     }
   },
 
   canDecrypt: function canDecrypt() {
     // Report failure even if there's nothing to decrypt
     let canDecrypt = false;
 
     // Fetch the most recently uploaded record and try to decrypt it
-    let test = new Collection(this.engineURL, this._recordObj);
+    let test = new Collection(this.engineURL, this._recordObj, this.service);
     test.limit = 1;
     test.sort = "newest";
     test.full = true;
-    test.recordHandler = function(record) {
-      record.decrypt();
+
+    let key = this.service.collectionKeys.keyForCollection(this.name);
+    test.recordHandler = function recordHandler(record) {
+      record.decrypt(key);
       canDecrypt = true;
-    };
+    }.bind(this);
 
     // Any failure fetching/decrypting will just result in false
     try {
       this._log.trace("Trying to decrypt a record from the server..");
       test.get();
     }
     catch(ex) {
       this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex));
@@ -1318,17 +1347,17 @@ SyncEngine.prototype = {
 
   _resetClient: function SyncEngine__resetClient() {
     this.resetLastSync();
     this.previousFailed = [];
     this.toFetch = [];
   },
 
   wipeServer: function wipeServer() {
-    let response = new Resource(this.engineURL).delete();
+    let response = this.service.resource(this.engineURL).delete();
     if (response.status != 200 && response.status != 404) {
       throw response;
     }
     this._resetClient();
   },
 
   removeClientData: function removeClientData() {
     // Implement this method in engines that store client specific data
@@ -1348,13 +1377,13 @@ SyncEngine.prototype = {
    *
    * Subclasses of SyncEngine can override this method to allow for different
    * behavior -- e.g., to delete and ignore erroneous entries.
    *
    * All return values will be part of the kRecoveryStrategy enumeration.
    */
   handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
     // By default we either try again, or bail out noisily.
-    return (Weave.Service.handleHMACEvent() && mayRetry) ?
+    return (this.service.handleHMACEvent() && mayRetry) ?
            SyncEngine.kRecoveryStrategy.retry :
            SyncEngine.kRecoveryStrategy.error;
   }
 };
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -102,18 +102,18 @@ Utils.deferGetSet(AddonRecord, "cleartex
  *
  * The engine maintains an instance of an AddonsReconciler, which is the entity
  * maintaining state for add-ons. It provides the history and tracking APIs
  * that AddonManager doesn't.
  *
  * The engine instance overrides a handful of functions on the base class. The
  * rationale for each is documented by that function.
  */
-function AddonsEngine() {
-  SyncEngine.call(this, "Addons");
+function AddonsEngine(service) {
+  SyncEngine.call(this, "Addons", service);
 
   this._reconciler = new AddonsReconciler();
 }
 AddonsEngine.prototype = {
   __proto__:              SyncEngine.prototype,
   _storeObj:              AddonsStore,
   _trackerObj:            AddonsTracker,
   _recordObj:             AddonRecord,
@@ -233,38 +233,31 @@ AddonsEngine.prototype = {
 };
 
 /**
  * This is the primary interface between Sync and the Addons Manager.
  *
  * In addition to the core store APIs, we provide convenience functions to wrap
  * Add-on Manager APIs with Sync-specific semantics.
  */
-function AddonsStore(name) {
-  Store.call(this, name);
+function AddonsStore(name, engine) {
+  Store.call(this, name, engine);
 }
 AddonsStore.prototype = {
   __proto__: Store.prototype,
 
   // Define the add-on types (.type) that we support.
   _syncableTypes: ["extension", "theme"],
 
   _extensionsPrefs: new Preferences("extensions."),
 
   get reconciler() {
     return this.engine._reconciler;
   },
 
-  get engine() {
-    // Ideally we'd link to a specific object, but the API doesn't provide an
-    // easy way to faciliate this. When the async API lands, this hackiness can
-    // go away.
-    return Engines.get("addons");
-  },
-
   /**
    * Override applyIncoming to filter out records we can't handle.
    */
   applyIncoming: function applyIncoming(record) {
     // The fields we look at aren't present when the record is deleted.
     if (!record.deleted) {
       // Ignore records not belonging to our application ID because that is the
       // current policy.
@@ -655,31 +648,31 @@ AddonsStore.prototype = {
   },
 };
 
 /**
  * The add-ons tracker keeps track of real-time changes to add-ons.
  *
  * It hooks up to the reconciler and receives notifications directly from it.
  */
-function AddonsTracker(name) {
-  Tracker.call(this, name);
+function AddonsTracker(name, engine) {
+  Tracker.call(this, name, engine);
 
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 AddonsTracker.prototype = {
   __proto__: Tracker.prototype,
 
   get reconciler() {
-    return Engines.get("addons")._reconciler;
+    return this.engine._reconciler;
   },
 
   get store() {
-    return Engines.get("addons")._store;
+    return this.engine._store;
   },
 
   /**
    * This callback is executed whenever the AddonsReconciler sends out a change
    * notification. See AddonsReconciler.addChangeListener().
    */
   changeListener: function changeHandler(date, change, addon) {
     this._log.debug("changeListener invoked: " + change + " " + addon.id);
@@ -696,17 +689,22 @@ AddonsTracker.prototype = {
 
     this.addChangedID(addon.guid, date.getTime() / 1000);
     this.score += SCORE_INCREMENT_XLARGE;
   },
 
   observe: function(subject, topic, data) {
     switch (topic) {
       case "weave:engine:start-tracking":
+        if (this.engine.enabled) {
+          this.reconciler.startListening();
+        }
+
         this.reconciler.addChangeListener(this);
         break;
 
       case "weave:engine:stop-tracking":
         this.reconciler.removeChangeListener(this);
+        this.reconciler.stopListening();
         break;
     }
   }
 };
--- a/services/sync/modules/engines/apps.js
+++ b/services/sync/modules/engines/apps.js
@@ -21,18 +21,18 @@ function AppRec(collection, id) {
 
 AppRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.App"
 }
 
 Utils.deferGetSet(AppRec, "cleartext", ["value"]);
 
-function AppStore(name) {
-  Store.call(this, name);
+function AppStore(name, engine) {
+  Store.call(this, name, engine);
 }
 
 AppStore.prototype = {
   __proto__: Store.prototype,
 
   getAllIDs: function getAllIDs() {
     let apps = DOMApplicationRegistry.getAllIDs();
     return apps;
@@ -76,18 +76,18 @@ AppStore.prototype = {
   wipe: function wipe(record) {
     let callback = Async.makeSyncCallback();
     DOMApplicationRegistry.wipe(callback);
     Async.waitForSyncCallback(callback);
   }
 }
 
 
-function AppTracker(name) {
-  Tracker.call(this, name);
+function AppTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 
 AppTracker.prototype = {
   __proto__: Tracker.prototype,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
   
@@ -118,18 +118,18 @@ AppTracker.prototype = {
         this._enabled = false;
         Svc.Obs.remove("webapps-sync-install", this);
         Svc.Obs.remove("webapps-sync-uninstall", this);
         break;
     }
   }
 }
 
-function AppsEngine() {
-  SyncEngine.call(this, "Apps");
+function AppsEngine(service) {
+  SyncEngine.call(this, "Apps", service);
 }
 
 AppsEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: AppStore,
   _trackerObj: AppTracker,
   _recordObj: AppRec,
   applyIncomingBatchSize: APPS_STORE_BATCH_SIZE
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -7,23 +7,21 @@ const EXPORTED_SYMBOLS = ['BookmarksEngi
                           "Livemark", "BookmarkSeparator"];
 
 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-common/async.js");
+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-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";
 const MOBILEROOT_ANNO      = "mobile/bookmarksRoot";
 const MOBILE_ANNO          = "MobileBookmarks";
 const EXCLUDEBACKUP_ANNO   = "places/excludeFromBackup";
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
@@ -188,18 +186,18 @@ let kSpecialIds = {
   get menu()    PlacesUtils.bookmarksMenuFolderId,
   get places()  PlacesUtils.placesRootId,
   get tags()    PlacesUtils.tagsFolderId,
   get toolbar() PlacesUtils.toolbarFolderId,
   get unfiled() PlacesUtils.unfiledBookmarksFolderId,
   get mobile()  this.findMobileRoot(true),
 };
 
-function BookmarksEngine() {
-  SyncEngine.call(this, "Bookmarks");
+function BookmarksEngine(service) {
+  SyncEngine.call(this, "Bookmarks", service);
 }
 BookmarksEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _recordObj: PlacesItem,
   _storeObj: BookmarksStore,
   _trackerObj: BookmarksTracker,
   version: 2,
 
@@ -405,18 +403,18 @@ BookmarksEngine.prototype = {
   _findDupe: function _findDupe(item) {
     // Don't bother finding a dupe if the incoming item has duplicates
     if (item.hasDupe)
       return;
     return this._mapDupe(item);
   }
 };
 
-function BookmarksStore(name) {
-  Store.call(this, name);
+function BookmarksStore(name, engine) {
+  Store.call(this, name, engine);
 
   // Explicitly nullify our references to our cached services so we don't leak
   Svc.Obs.add("places-shutdown", function() {
     for each ([query, stmt] in Iterator(this._stmts)) {
       stmt.finalize();
     }
     this._stmts = {};
   }, this);
@@ -1250,18 +1248,18 @@ BookmarksStore.prototype = {
       if (guid != "places") {
         let id = kSpecialIds.specialIdForGUID(guid);
         if (id)
           PlacesUtils.bookmarks.removeFolderChildren(id);
       }
   }
 };
 
-function BookmarksTracker(name) {
-  Tracker.call(this, name);
+function BookmarksTracker(name, engine) {
+  Tracker.call(this, name, engine);
 
   Svc.Obs.add("places-shutdown", this);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 BookmarksTracker.prototype = {
   __proto__: Tracker.prototype,
 
@@ -1281,29 +1279,29 @@ BookmarksTracker.prototype = {
         if (this._enabled) {
           PlacesUtils.bookmarks.removeObserver(this);
           Svc.Obs.remove("bookmarks-restore-begin", this);
           Svc.Obs.remove("bookmarks-restore-success", this);
           Svc.Obs.remove("bookmarks-restore-failed", this);
           this._enabled = false;
         }
         break;
-        
+
       case "bookmarks-restore-begin":
         this._log.debug("Ignoring changes from importing bookmarks.");
         this.ignoreAll = true;
         break;
       case "bookmarks-restore-success":
         this._log.debug("Tracking all items on successful import.");
         this.ignoreAll = false;
-        
+
         this._log.debug("Restore succeeded: wiping server and other clients.");
-        Weave.Service.resetClient([this.name]);
-        Weave.Service.wipeServer([this.name]);
-        Clients.sendCommand("wipeEngine", [this.name]);
+        this.engine.service.resetClient([this.name]);
+        this.engine.service.wipeServer([this.name]);
+        this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]);
         break;
       case "bookmarks-restore-failed":
         this._log.debug("Tracking all items on failed import.");
         this.ignoreAll = false;
         break;
     }
   },
 
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -1,47 +1,42 @@
 /* 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 = ["Clients", "ClientsRec"];
+const EXPORTED_SYMBOLS = [
+  "ClientEngine",
+  "ClientsRec"
+];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 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/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
 
 function ClientsRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 ClientsRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.Clients",
   ttl: CLIENTS_TTL
 };
 
 Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands"]);
 
 
-XPCOMUtils.defineLazyGetter(this, "Clients", function () {
-  return new ClientEngine();
-});
-
-function ClientEngine() {
-  SyncEngine.call(this, "Clients");
+function ClientEngine(service) {
+  SyncEngine.call(this, "Clients", service);
 
   // Reset the client on every startup so that we fetch recent clients
   this._resetClient();
 }
 ClientEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: ClientStore,
   _recordObj: ClientsRec,
@@ -130,17 +125,17 @@ ClientEngine.prototype = {
   _resetClient: function _resetClient() this._wipeClient(),
 
   _wipeClient: function _wipeClient() {
     SyncEngine.prototype._resetClient.call(this);
     this._store.wipe();
   },
 
   removeClientData: function removeClientData() {
-    let res = new Resource(this.engineURL + "/" + this.localID);
+    let res = this.service.resource(this.engineURL + "/" + this.localID);
     res.delete();
   },
 
   // Override the default behavior to delete bad records from the server.
   handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
     this._log.debug("Handling HMAC mismatch for " + item.id);
 
     let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
@@ -235,26 +230,26 @@ ClientEngine.prototype = {
         this._log.debug("Processing command: " + command + "(" + args + ")");
 
         let engines = [args[0]];
         switch (command) {
           case "resetAll":
             engines = null;
             // Fallthrough
           case "resetEngine":
-            Weave.Service.resetClient(engines);
+            this.service.resetClient(engines);
             break;
           case "wipeAll":
             engines = null;
             // Fallthrough
           case "wipeEngine":
-            Weave.Service.wipeClient(engines);
+            this.service.wipeClient(engines);
             break;
           case "logout":
-            Weave.Service.logout();
+            this.service.logout();
             return false;
           case "displayURI":
             this._handleDisplayURI.apply(this, args);
             break;
           default:
             this._log.debug("Received an unknown command: " + command);
             break;
         }
@@ -319,17 +314,17 @@ ClientEngine.prototype = {
    * @param title
    *        Title of the page being sent.
    */
   sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
     this._log.info("Sending URI to client: " + uri + " -> " +
                    clientId + " (" + title + ")");
     this.sendCommand("displayURI", [uri, this.localID, title], clientId);
 
-    Clients._tracker.score += SCORE_INCREMENT_XLARGE;
+    this._tracker.score += SCORE_INCREMENT_XLARGE;
   },
 
   /**
    * Handle a single received 'displayURI' command.
    *
    * Interested parties should observe the "weave:engine:clients:display-uri"
    * topic. The callback will receive an object as the subject parameter with
    * the following keys:
@@ -352,64 +347,64 @@ ClientEngine.prototype = {
     this._log.info("Received a URI for display: " + uri + " (" + title +
                    ") from " + clientId);
 
     let subject = {uri: uri, client: clientId, title: title};
     Svc.Obs.notify("weave:engine:clients:display-uri", subject);
   }
 };
 
-function ClientStore(name) {
-  Store.call(this, name);
+function ClientStore(name, engine) {
+  Store.call(this, name, engine);
 }
 ClientStore.prototype = {
   __proto__: Store.prototype,
 
   create: function create(record) this.update(record),
 
   update: function update(record) {
     // Only grab commands from the server; local name/type always wins
-    if (record.id == Clients.localID)
-      Clients.localCommands = record.commands;
+    if (record.id == this.engine.localID)
+      this.engine.localCommands = record.commands;
     else
       this._remoteClients[record.id] = record.cleartext;
   },
 
   createRecord: function createRecord(id, collection) {
     let record = new ClientsRec(collection, id);
 
     // Package the individual components into a record for the local client
-    if (id == Clients.localID) {
-      record.name = Clients.localName;
-      record.type = Clients.localType;
-      record.commands = Clients.localCommands;
+    if (id == this.engine.localID) {
+      record.name = this.engine.localName;
+      record.type = this.engine.localType;
+      record.commands = this.engine.localCommands;
     }
     else
       record.cleartext = this._remoteClients[id];
 
     return record;
   },
 
   itemExists: function itemExists(id) id in this.getAllIDs(),
 
   getAllIDs: function getAllIDs() {
     let ids = {};
-    ids[Clients.localID] = true;
+    ids[this.engine.localID] = true;
     for (let id in this._remoteClients)
       ids[id] = true;
     return ids;
   },
 
   wipe: function wipe() {
     this._remoteClients = {};
   },
 };
 
-function ClientsTracker(name) {
-  Tracker.call(this, name);
+function ClientsTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 ClientsTracker.prototype = {
   __proto__: Tracker.prototype,
 
   _enabled: false,
 
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -146,18 +146,18 @@ let FormWrapper = {
     let stmt = this._replaceGUIDStmt;
     stmt.params.oldGUID = oldGUID;
     stmt.params.newGUID = newGUID;
     Async.querySpinningly(stmt);
   }
 
 };
 
-function FormEngine() {
-  SyncEngine.call(this, "Forms");
+function FormEngine(service) {
+  SyncEngine.call(this, "Forms", service);
 }
 FormEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: FormStore,
   _trackerObj: FormTracker,
   _recordObj: FormRec,
   applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
 
@@ -165,18 +165,18 @@ FormEngine.prototype = {
 
   _findDupe: function _findDupe(item) {
     if (Svc.Form.entryExists(item.name, item.value)) {
       return FormWrapper.getGUID(item.name, item.value);
     }
   }
 };
 
-function FormStore(name) {
-  Store.call(this, name);
+function FormStore(name, engine) {
+  Store.call(this, name, engine);
 }
 FormStore.prototype = {
   __proto__: Store.prototype,
 
   applyIncomingBatch: function applyIncomingBatch(records) {
     return Utils.runInTransaction(Svc.Form.DBConnection, function() {
       return Store.prototype.applyIncomingBatch.call(this, records);
     }, this);
@@ -236,18 +236,18 @@ FormStore.prototype = {
     this._log.trace("Ignoring form record update request!");
   },
 
   wipe: function FormStore_wipe() {
     Svc.Form.removeAllEntries();
   }
 };
 
-function FormTracker(name) {
-  Tracker.call(this, name);
+function FormTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
   Svc.Obs.add("profile-change-teardown", this);
 }
 FormTracker.prototype = {
   __proto__: Tracker.prototype,
 
   QueryInterface: XPCOMUtils.generateQI([
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -7,49 +7,49 @@ const EXPORTED_SYMBOLS = ['HistoryEngine
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 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-common/async.js");
+Cu.import("resource://services-common/log4moz.js");
 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-common/async.js");
 Cu.import("resource://services-sync/util.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
 };
 
 Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]);
 
 
-function HistoryEngine() {
-  SyncEngine.call(this, "History");
+function HistoryEngine(service) {
+  SyncEngine.call(this, "History", service);
 }
 HistoryEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _recordObj: HistoryRec,
   _storeObj: HistoryStore,
   _trackerObj: HistoryTracker,
   downloadLimit: MAX_HISTORY_DOWNLOAD,
   applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE
 };
 
-function HistoryStore(name) {
-  Store.call(this, name);
+function HistoryStore(name, engine) {
+  Store.call(this, name, engine);
 
   // Explicitly nullify our references to our cached services so we don't leak
   Svc.Obs.add("places-shutdown", function() {
     for each ([query, stmt] in Iterator(this._stmts)) {
       stmt.finalize();
     }
     this._stmts = {};
   }, this);
@@ -349,18 +349,18 @@ HistoryStore.prototype = {
     return record;
   },
 
   wipe: function HistStore_wipe() {
     PlacesUtils.history.removeAllPages();
   }
 };
 
-function HistoryTracker(name) {
-  Tracker.call(this, name);
+function HistoryTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 HistoryTracker.prototype = {
   __proto__: Tracker.prototype,
 
   _enabled: false,
   observe: function observe(subject, topic, data) {
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -21,18 +21,18 @@ LoginRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.Login",
 };
 
 Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL",
   "httpRealm", "username", "password", "usernameField", "passwordField"]);
 
 
-function PasswordEngine() {
-  SyncEngine.call(this, "Passwords");
+function PasswordEngine(service) {
+  SyncEngine.call(this, "Passwords", service);
 }
 PasswordEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: PasswordStore,
   _trackerObj: PasswordTracker,
   _recordObj: LoginRec,
   applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
 
@@ -41,17 +41,17 @@ PasswordEngine.prototype = {
 
     // Delete the weave credentials from the server once
     if (!Svc.Prefs.get("deletePwd", false)) {
       try {
         let ids = Services.logins.findLogins({}, PWDMGR_HOST, "", "")
                           .map(function(info) {
           return info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid;
         });
-        let coll = new Collection(this.engineURL);
+        let coll = new Collection(this.engineURL, null, this.service);
         coll.ids = ids;
         let ret = coll.delete();
         this._log.debug("Delete result: " + ret);
 
         Svc.Prefs.set("deletePwd", true);
       }
       catch(ex) {
         this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
@@ -70,18 +70,18 @@ PasswordEngine.prototype = {
 
     // Look for existing logins that match the hostname but ignore the password
     for each (let local in logins)
       if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo)
         return local.guid;
   }
 };
 
-function PasswordStore(name) {
-  Store.call(this, name);
+function PasswordStore(name, engine) {
+  Store.call(this, name, engine);
   this._nsLoginInfo = new Components.Constructor(
     "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
 
   XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
     return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.mozIStorageConnection);
   });
 }
@@ -250,18 +250,18 @@ PasswordStore.prototype = {
     }
   },
 
   wipe: function PasswordStore_wipe() {
     Services.logins.removeAllLogins();
   }
 };
 
-function PasswordTracker(name) {
-  Tracker.call(this, name);
+function PasswordTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 PasswordTracker.prototype = {
   __proto__: Tracker.prototype,
 
   _enabled: false,
   observe: function PasswordTracker_observe(aSubject, aTopic, aData) {
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -26,18 +26,18 @@ function PrefRec(collection, id) {
 PrefRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.Pref",
 };
 
 Utils.deferGetSet(PrefRec, "cleartext", ["value"]);
 
 
-function PrefsEngine() {
-  SyncEngine.call(this, "Prefs");
+function PrefsEngine(service) {
+  SyncEngine.call(this, "Prefs", service);
 }
 PrefsEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: PrefStore,
   _trackerObj: PrefTracker,
   _recordObj: PrefRec,
   version: 2,
 
@@ -60,18 +60,18 @@ PrefsEngine.prototype = {
       this.justWiped = false;
       return true;
     }
     return SyncEngine.prototype._reconcile.call(this, item);
   }
 };
 
 
-function PrefStore(name) {
-  Store.call(this, name);
+function PrefStore(name, engine) {
+  Store.call(this, name, engine);
   Svc.Obs.add("profile-before-change", function() {
     this.__prefs = null;
   }, this);
 }
 PrefStore.prototype = {
   __proto__: Store.prototype,
 
  __prefs: null,
@@ -183,18 +183,18 @@ PrefStore.prototype = {
     this._setAllPrefs(record.value);
   },
 
   wipe: function PrefStore_wipe() {
     this._log.trace("Ignoring wipe request");
   }
 };
 
-function PrefTracker(name) {
-  Tracker.call(this, name);
+function PrefTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("profile-before-change", this);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 PrefTracker.prototype = {
   __proto__: Tracker.prototype,
 
   get modified() {
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -9,17 +9,16 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 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-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
@@ -34,33 +33,33 @@ TabSetRecord.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.Tabs",
   ttl: TABS_TTL
 };
 
 Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
 
 
-function TabEngine() {
-  SyncEngine.call(this, "Tabs");
+function TabEngine(service) {
+  SyncEngine.call(this, "Tabs", service);
 
   // Reset the client on every startup so that we fetch recent tabs
   this._resetClient();
 }
 TabEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: TabStore,
   _trackerObj: TabTracker,
   _recordObj: TabSetRecord,
 
   getChangedIDs: function getChangedIDs() {
     // No need for a proper timestamp (no conflict resolution needed).
     let changedIDs = {};
     if (this._tracker.modified)
-      changedIDs[Clients.localID] = 0;
+      changedIDs[this.service.clientsEngine.localID] = 0;
     return changedIDs;
   },
 
   // API for use by Weave UI code to give user choices of tabs to open:
   getAllClients: function TabEngine_getAllClients() {
     return this._store._remoteClients;
   },
 
@@ -70,17 +69,18 @@ TabEngine.prototype = {
 
   _resetClient: function TabEngine__resetClient() {
     SyncEngine.prototype._resetClient.call(this);
     this._store.wipe();
     this._tracker.modified = true;
   },
 
   removeClientData: function removeClientData() {
-    new Resource(this.engineURL + "/" + Clients.localID).delete();
+    let url = this.engineURL + "/" + this.service.clientsEngine.localID;
+    this.service.resource(url).delete();
   },
 
   /* The intent is not to show tabs in the menu if they're already
    * open locally.  There are a couple ways to interpret this: for
    * instance, we could do it by removing a tab from the list when
    * you open it -- but then if you close it, you can't get back to
    * it.  So the way I'm doing it here is to not show a tab in the menu
    * if you have a tab open to the same URL, even though this means
@@ -90,24 +90,24 @@ TabEngine.prototype = {
   locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) {
     return this._store.getAllTabs().some(function(tab) {
       return tab.urlHistory[0] == url;
     });
   }
 };
 
 
-function TabStore(name) {
-  Store.call(this, name);
+function TabStore(name, engine) {
+  Store.call(this, name, engine);
 }
 TabStore.prototype = {
   __proto__: Store.prototype,
 
   itemExists: function TabStore_itemExists(id) {
-    return id == Clients.localID;
+    return id == this.engine.service.clientsEngine.localID;
   },
 
   /**
    * Return the recorded last used time of the provided tab, or
    * 0 if none is present.
    * The result will always be an integer value.
    */
   tabLastUsed: function tabLastUsed(tab) {
@@ -152,17 +152,17 @@ TabStore.prototype = {
       });
     });
 
     return allTabs;
   },
 
   createRecord: function createRecord(id, collection) {
     let record = new TabSetRecord(collection, id);
-    record.clientName = Clients.localName;
+    record.clientName = this.engine.service.clientsEngine.localName;
 
     // Don't provide any tabs to compare against and ignore the update later.
     if (Svc.Private && Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) {
       record.tabs = [];
       return record;
     }
 
     // Sort tabs in descending-used order to grab the most recently used
@@ -195,17 +195,17 @@ TabStore.prototype = {
   },
 
   getAllIDs: function TabStore_getAllIds() {
     // Don't report any tabs if we're in private browsing for first syncs.
     let ids = {};
     if (Svc.Private && Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart"))
       return ids;
 
-    ids[Clients.localID] = true;
+    ids[this.engine.service.clientsEngine.localID] = true;
     return ids;
   },
 
   wipe: function TabStore_wipe() {
     this._remoteClients = {};
   },
 
   create: function TabStore_create(record) {
@@ -227,18 +227,18 @@ TabStore.prototype = {
   },
 
   update: function update(record) {
     this._log.trace("Ignoring tab updates as local ones win");
   }
 };
 
 
-function TabTracker(name) {
-  Tracker.call(this, name);
+function TabTracker(name, engine) {
+  Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 
   // Make sure "this" pointer is always set correctly for event listeners
   this.onTab = Utils.bind2(this, this.onTab);
   this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
 }
 TabTracker.prototype = {
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -1,46 +1,37 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const EXPORTED_SYMBOLS = ["Identity", "IdentityManager"];
+const EXPORTED_SYMBOLS = ["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-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
-XPCOMUtils.defineLazyGetter(this, "Identity", function() {
-  return new IdentityManager();
-});
-
 /**
  * Manages identity and authentication for Sync.
  *
  * The following entities are managed:
  *
  *   account - The main Sync/services account. This is typically an email
  *     address.
  *   username - A normalized version of your account. This is what's
  *     transmitted to the server.
  *   basic password - UTF-8 password used for authenticating when using HTTP
  *     basic authentication.
  *   sync key - The main encryption key used by Sync.
  *   sync key bundle - A representation of your sync key.
  *
- * An instance of this type is lazily instantiated under Weave.Identity. It is
- * and should be treated as a global variable. The reason is that saved changes
- * are stored in preferences and the password manager. So, if you created
- * multiple instances, they would just step on each other's state.
- *
  * When changes are made to entities that are stored in the password manager
  * (basic password, sync key), those changes are merely staged. To commit them
  * to the password manager, you'll need to call persistCredentials().
  *
  * This type also manages authenticating Sync's network requests. Sync's
  * network code calls into getRESTRequestAuthenticator and
  * getResourceAuthenticator (depending on the network layer being used). Each
  * returns a function which can be used to add authentication information to an
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -1,24 +1,21 @@
 /* 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 Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 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;
 
 const JPAKE_SIGNERID_SENDER   = "sender";
 const JPAKE_SIGNERID_RECEIVER = "receiver";
 const JPAKE_LENGTH_SECRET     = 8;
 const JPAKE_LENGTH_CLIENTID   = 256;
 const JPAKE_VERIFY_VALUE      = "0123456789ABCDEF";
@@ -676,8 +673,102 @@ JPAKEClient.prototype = {
   _complete: function _complete() {
     this._log.debug("Exchange completed.");
     this._finished = true;
     Utils.nextTick(function () { this.controller.onComplete(this._newData); },
                    this);
   }
 
 };
+
+
+/**
+ * Send credentials over an active J-PAKE channel.
+ *
+ * This object is designed to take over as the JPAKEClient controller,
+ * presumably replacing one that is UI-based which would either cause
+ * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
+ * context disappears. This object stays alive for the duration of the
+ * transfer by being strong-ref'ed as an nsIObserver.
+ *
+ * Credentials are sent after the first sync has been completed
+ * (successfully or not.)
+ *
+ * Usage:
+ *
+ *   jpakeclient.controller = new SendCredentialsController(jpakeclient,
+ *                                                          service);
+ *
+ */
+function SendCredentialsController(jpakeclient, service) {
+  this._log = Log4Moz.repository.getLogger("Sync.SendCredentialsController");
+  this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+
+  this._log.trace("Loading.");
+  this.jpakeclient = jpakeclient;
+  this.service = service;
+
+  // Register ourselves as observers the first Sync finishing (either
+  // successfully or unsuccessfully, we don't care) or for removing
+  // this device's sync configuration, in case that happens while we
+  // haven't finished the first sync yet.
+  Services.obs.addObserver(this, "weave:service:sync:finish", false);
+  Services.obs.addObserver(this, "weave:service:sync:error",  false);
+  Services.obs.addObserver(this, "weave:service:start-over",  false);
+}
+SendCredentialsController.prototype = {
+
+  unload: function unload() {
+    this._log.trace("Unloading.");
+    try {
+      Services.obs.removeObserver(this, "weave:service:sync:finish");
+      Services.obs.removeObserver(this, "weave:service:sync:error");
+      Services.obs.removeObserver(this, "weave:service:start-over");
+    } catch (ex) {
+      // Ignore.
+    }
+  },
+
+  observe: function observe(subject, topic, data) {
+    switch (topic) {
+      case "weave:service:sync:finish":
+      case "weave:service:sync:error":
+        Utils.nextTick(this.sendCredentials, this);
+        break;
+      case "weave:service:start-over":
+        // This will call onAbort which will call unload().
+        this.jpakeclient.abort();
+        break;
+    }
+  },
+
+  sendCredentials: function sendCredentials() {
+    this._log.trace("Sending credentials.");
+    let credentials = {account:   this.service.identity.account,
+                       password:  this.service.identity.basicPassword,
+                       synckey:   this.service.identity.syncKey,
+                       serverURL: this.service.serverURL};
+    this.jpakeclient.sendAndComplete(credentials);
+  },
+
+  // JPAKEClient controller API
+
+  onComplete: function onComplete() {
+    this._log.debug("Exchange was completed successfully!");
+    this.unload();
+
+    // Schedule a Sync for soonish to fetch the data uploaded by the
+    // device with which we just paired.
+    this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
+  },
+
+  onAbort: function onAbort(error) {
+    // It doesn't really matter why we aborted, but the channel is closed
+    // for sure, so we won't be able to do anything with it.
+    this._log.debug("Exchange was aborted with error: " + error);
+    this.unload();
+  },
+
+  // Irrelevant methods for this controller:
+  displayPIN: function displayPIN() {},
+  onPairingStart: function onPairingStart() {},
+  onPaired: function onPaired() {},
+};
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -2,37 +2,21 @@
  * 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 = ['Weave'];
 
 let Weave = {};
 Components.utils.import("resource://services-sync/constants.js", Weave);
 let lazies = {
-  "record.js":            ["CollectionKeys"],
-  "engines.js":           ['Engines', 'Engine', 'SyncEngine', 'Store'],
-  "engines/addons.js":    ["AddonsEngine"],
-  "engines/bookmarks.js": ['BookmarksEngine', 'BookmarksSharingManager'],
-  "engines/clients.js":   ["Clients"],
-  "engines/forms.js":     ["FormEngine"],
-  "engines/history.js":   ["HistoryEngine"],
-  "engines/prefs.js":     ["PrefsEngine"],
-  "engines/passwords.js": ["PasswordEngine"],
-  "engines/tabs.js":      ["TabEngine"],
-  "engines/apps.js":      ["AppsEngine"],
-  "identity.js":          ["Identity"],
-  "jpakeclient.js":       ["JPAKEClient"],
-  "keys.js":              ["BulkKeyBundle", "SyncKeyBundle"],
+  "jpakeclient.js":       ["JPAKEClient", "SendCredentialsController"],
   "notifications.js":     ["Notifications", "Notification", "NotificationButton"],
-  "policies.js":          ["SyncScheduler", "ErrorHandler",
-                           "SendCredentialsController"],
-  "resource.js":          ["Resource", "AsyncResource"],
   "service.js":           ["Service"],
   "status.js":            ["Status"],
-  "util.js":              ['Utils', 'Svc', 'Str']
+  "util.js":              ['Utils', 'Svc']
 };
 
 function lazyImport(module, dest, props) {
   function getter(prop) function() {
     let ns = {};
     Components.utils.import(module, ns);
     delete dest[prop];
     return dest[prop] = ns[prop];
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -1,28 +1,30 @@
 /* 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 = ["SyncScheduler",
-                          "ErrorHandler",
-                          "SendCredentialsController"];
+const EXPORTED_SYMBOLS = [
+  "ErrorHandler",
+  "SyncScheduler",
+];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/constants.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/util.js");
 
-Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
-
-let SyncScheduler = {
+function SyncScheduler(service) {
+  this.service = service;
+  this.init();
+}
+SyncScheduler.prototype = {
   _log: Log4Moz.repository.getLogger("Sync.SyncScheduler"),
 
   _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
                       LOGIN_FAILED_NO_PASSWORD,
                       LOGIN_FAILED_NO_PASSPHRASE,
                       LOGIN_FAILED_INVALID_PASSPHRASE,
                       LOGIN_FAILED_LOGIN_REJECTED],
 
@@ -192,17 +194,17 @@ let SyncScheduler = {
           this.hasIncomingItems = true;
         }
         break;
       case "weave:service:setup-complete":
          Services.prefs.savePrefFile(null);
          Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
          break;
       case "weave:service:start-over":
-         SyncScheduler.setDefaults();
+         this.setDefaults();
          try {
            Svc.Idle.removeIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
          } catch (ex if (ex.result == Cr.NS_ERROR_FAILURE)) {
            // In all likelihood we didn't have an idle observer registered yet.
            // It's all good.
          }
          break;
       case "idle":
@@ -253,33 +255,33 @@ let SyncScheduler = {
       this.syncInterval = this.immediateInterval;
     } else {
       this._log.trace("Adjusting syncInterval to activeInterval.");
       this.syncInterval = this.activeInterval;
     }
   },
 
   calculateScore: function calculateScore() {
-    let engines = [Clients].concat(Engines.getEnabled());
+    let engines = [this.service.clientsEngine].concat(this.service.engineManager.getEnabled());
     for (let i = 0;i < engines.length;i++) {
       this._log.trace(engines[i].name + ": score: " + engines[i].score);
       this.globalScore += engines[i].score;
       engines[i]._tracker.resetScore();
     }
 
     this._log.trace("Global score updated: " + this.globalScore);
     this.checkSyncStatus();
   },
 
   /**
    * Process the locally stored clients list to figure out what mode to be in
    */
   updateClientMode: function updateClientMode() {
     // Nothing to do if it's the same amount
-    let numClients = Clients.stats.numClients;
+    let numClients = this.service.clientsEngine.stats.numClients;
     if (this.numClients == numClients)
       return;
 
     this._log.debug("Client count: " + this.numClients + " -> " + numClients);
     this.numClients = numClients;
 
     if (numClients <= 1) {
       this._log.trace("Adjusting syncThreshold to SINGLE_USER_THRESHOLD");
@@ -293,17 +295,17 @@ let SyncScheduler = {
 
   /**
    * Check if we should be syncing and schedule the next sync, if it's not scheduled
    */
   checkSyncStatus: function checkSyncStatus() {
     // Should we be syncing now, if not, cancel any sync timers and return
     // if we're in backoff, we'll schedule the next sync.
     let ignore = [kSyncBackoffNotMet, kSyncMasterPasswordLocked];
-    let skip = Weave.Service._checkSync(ignore);
+    let skip = this.service._checkSync(ignore);
     this._log.trace("_checkSync returned \"" + skip + "\".");
     if (skip) {
       this.clearSyncTriggers();
       return;
     }
 
     // Only set the wait time to 0 if we need to sync right away
     let wait;
@@ -326,32 +328,32 @@ let SyncScheduler = {
       this._log.debug("Not initiating sync: Login status is " + Status.login);
 
       // If we're not syncing now, we need to schedule the next one.
       this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL");
       this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
       return;
     }
 
-    Utils.nextTick(Weave.Service.sync, Weave.Service);
+    Utils.nextTick(this.service.sync, this.service);
   },
 
   /**
    * Set a timer for the next sync
    */
   scheduleNextSync: function scheduleNextSync(interval) {
     // If no interval was specified, use the current sync interval.
     if (interval == null) {
       interval = this.syncInterval;
     }
 
     // Ensure the interval is set to no less than the backoff.
     if (Status.backoffInterval && interval < Status.backoffInterval) {
       this._log.trace("Requested interval " + interval +
-                      " ms is smaller than the backoff interval. " + 
+                      " ms is smaller than the backoff interval. " +
                       "Using backoff interval " +
                       Status.backoffInterval + " ms instead.");
       interval = Status.backoffInterval;
     }
 
     if (this.nextSync != 0) {
       // There's already a sync scheduled. Don't reschedule if there's already
       // a timer scheduled for sooner than requested.
@@ -401,23 +403,23 @@ let SyncScheduler = {
   * Automatically start syncing after the given delay (in seconds).
   *
   * Applications can define the `services.sync.autoconnectDelay` preference
   * to have this called automatically during start-up with the pref value as
   * the argument. Alternatively, they can call it themselves to control when
   * Sync should first start to sync.
   */
   delayedAutoConnect: function delayedAutoConnect(delay) {
-    if (Weave.Service._checkSetup() == STATUS_OK) {
+    if (this.service._checkSetup() == STATUS_OK) {
       Utils.namedTimer(this.autoConnect, delay * 1000, this, "_autoTimer");
     }
   },
 
   autoConnect: function autoConnect() {
-    if (Weave.Service._checkSetup() == STATUS_OK && !Weave.Service._checkSync()) {
+    if (this.service._checkSetup() == STATUS_OK && !this.service._checkSync()) {
       // Schedule a sync based on when a previous sync was scheduled.
       // scheduleNextSync() will do the right thing if that time lies in
       // the past.
       this.scheduleNextSync(this.nextSync - Date.now());
     }
 
     // Once autoConnect is called we no longer need _autoTimer.
     if (this._autoTimer) {
@@ -454,24 +456,27 @@ let SyncScheduler = {
    */
   clearSyncTriggers: function clearSyncTriggers() {
     this._log.debug("Clearing sync triggers and the global score.");
     this.globalScore = this.nextSync = 0;
 
     // Clear out any scheduled syncs
     if (this.syncTimer)
       this.syncTimer.clear();
-  }
-
+  },
 };
 
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 
-let ErrorHandler = {
+function ErrorHandler(service) {
+  this.service = service;
+  this.init();
+}
+ErrorHandler.prototype = {
 
   /**
    * Flag that turns on error reporting for all errors, incl. network errors.
    */
   dontIgnoreErrors: false,
 
   init: function init() {
     Svc.Obs.add("weave:engine:sync:applied", this);
@@ -535,17 +540,17 @@ let ErrorHandler = {
         } else {
           this.notifyOnNextTick("weave:ui:clear-error");
         }
 
         this.dontIgnoreErrors = false;
         break;
       case "weave:service:sync:error":
         if (Status.sync == CREDENTIALS_CHANGED) {
-          Weave.Service.logout();
+          this.service.logout();
         }
 
         this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                           LOG_PREFIX_ERROR);
 
         if (this.shouldReportError()) {
           this.notifyOnNextTick("weave:ui:sync:error");
         } else {
@@ -599,17 +604,17 @@ let ErrorHandler = {
 
   /**
    * Trigger a sync and don't muffle any errors, particularly network errors.
    */
   syncAndReportErrors: function syncAndReportErrors() {
     this._log.debug("Beginning user-triggered sync.");
 
     this.dontIgnoreErrors = true;
-    Utils.nextTick(Weave.Service.sync, Weave.Service);
+    Utils.nextTick(this.service.sync, this.service);
   },
 
   /**
    * Finds all logs older than maxErrorAge and deletes them without tying up I/O.
    */
   cleanupLogs: function cleanupLogs() {
     let direntries = FileUtils.getDir("ProfD", ["weave", "logs"]).directoryEntries;
     let oldLogs = [];
@@ -621,33 +626,33 @@ let ErrorHandler = {
       if (logFile.lastModifiedTime < threshold) {
         oldLogs.push(logFile);
       }
     }
 
     // Deletes a file from oldLogs each tick until there are none left.
     function deleteFile() {
       if (index >= oldLogs.length) {
-        ErrorHandler._cleaningUpFileLogs = false;
+        this._cleaningUpFileLogs = false;
         Svc.Obs.notify("weave:service:cleanup-logs");
         return;
       }
       try {
         oldLogs[index].remove(false);
       } catch (ex) {
-        ErrorHandler._log._debug("Encountered error trying to clean up old log file '"
-                                 + oldLogs[index].leafName + "':"
-                                 + Utils.exceptionStr(ex));
+        this._log._debug("Encountered error trying to clean up old log file '"
+                         + oldLogs[index].leafName + "':"
+                         + Utils.exceptionStr(ex));
       }
       index++;
       Utils.nextTick(deleteFile);
     }
 
     if (oldLogs.length > 0) {
-      ErrorHandler._cleaningUpFileLogs = true;
+      this._cleaningUpFileLogs = true;
       Utils.nextTick(deleteFile);
     }
   },
 
   /**
    * Generate a log file for the sync that just completed
    * and refresh the input & output streams.
    *
@@ -661,23 +666,24 @@ let ErrorHandler = {
   resetFileLog: function resetFileLog(flushToFile, filenamePrefix) {
     let inStream = this._logAppender.getInputStream();
     this._logAppender.reset();
     if (flushToFile && inStream) {
       try {
         let filename = filenamePrefix + Date.now() + ".txt";
         let file = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
         let outStream = FileUtils.openFileOutputStream(file);
-        NetUtil.asyncCopy(inStream, outStream, function () {
+
+        NetUtil.asyncCopy(inStream, outStream, function onCopyComplete() {
           Svc.Obs.notify("weave:service:reset-file-log");
-          if (filenamePrefix == LOG_PREFIX_ERROR
-              && !ErrorHandler._cleaningUpFileLogs) {
-            Utils.nextTick(ErrorHandler.cleanupLogs, ErrorHandler);
+          if (filenamePrefix == LOG_PREFIX_ERROR &&
+              !this._cleaningUpFileLogs) {
+            Utils.nextTick(this.cleanupLogs, this);
           }
-        });
+        }.bind(this));
       } catch (ex) {
         Svc.Obs.notify("weave:service:reset-file-log");
       }
     } else {
       Svc.Obs.notify("weave:service:reset-file-log");
     }
   },
 
@@ -724,21 +730,21 @@ let ErrorHandler = {
 
     let lastSync = Svc.Prefs.get("lastSync");
     if (lastSync && ((Date.now() - Date.parse(lastSync)) >
         Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) {
       Status.sync = PROLONGED_SYNC_FAILURE;
       this._log.trace("shouldReportError: true (prolonged sync failure).");
       return true;
     }
- 
+
     // We got a 401 mid-sync. Wait for the next sync before actually handling
     // an error. This assumes that we'll get a 401 again on a login fetch in
     // order to report the error.
-    if (!Weave.Service.clusterURL) {
+    if (!this.service.clusterURL) {
       this._log.trace("shouldReportError: false (no cluster URL; " +
                       "possible node reassignment).");
       return false;
     }
 
     return ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 &&
             [Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
   },
@@ -751,17 +757,17 @@ let ErrorHandler = {
     switch (resp.status) {
       case 400:
         if (resp == RESPONSE_OVER_QUOTA) {
           Status.sync = OVER_QUOTA;
         }
         break;
 
       case 401:
-        Weave.Service.logout();
+        this.service.logout();
         this._log.info("Got 401 response; resetting clusterURL.");
         Svc.Prefs.reset("clusterURL");
 
         let delay = 0;
         if (Svc.Prefs.get("lastSyncReassigned")) {
           // We got a 401 in the middle of the previous sync, and we just got
           // another. Login must have succeeded in order for us to get here, so
           // the password should be correct.
@@ -769,26 +775,26 @@ let ErrorHandler = {
           // give it time to recover.
           this._log.warn("Last sync also failed for 401. Delaying next sync.");
           delay = MINIMUM_BACKOFF_INTERVAL;
         } else {
           this._log.debug("New mid-sync 401 failure. Making a note.");
           Svc.Prefs.set("lastSyncReassigned", true);
         }
         this._log.info("Attempting to schedule another sync.");
-        SyncScheduler.scheduleNextSync(delay);
+        this.service.scheduler.scheduleNextSync(delay);
         break;
 
       case 500:
       case 502:
       case 503:
       case 504:
         Status.enforceBackoff = true;
         if (resp.status == 503 && resp.headers["retry-after"]) {
-          if (Weave.Service.isLoggedIn) {
+          if (this.service.isLoggedIn) {
             Status.sync = SERVER_MAINTENANCE;
           } else {
             Status.login = SERVER_MAINTENANCE;
           }
           Svc.Obs.notify("weave:service:backoff:interval",
                          parseInt(resp.headers["retry-after"], 10));
         }
         break;
@@ -798,109 +804,17 @@ let ErrorHandler = {
       case Cr.NS_ERROR_UNKNOWN_HOST:
       case Cr.NS_ERROR_CONNECTION_REFUSED:
       case Cr.NS_ERROR_NET_TIMEOUT:
       case Cr.NS_ERROR_NET_RESET:
       case Cr.NS_ERROR_NET_INTERRUPT:
       case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
         // The constant says it's about login, but in fact it just
         // indicates general network error.
-        if (Weave.Service.isLoggedIn) {
+        if (this.service.isLoggedIn) {
           Status.sync = LOGIN_FAILED_NETWORK_ERROR;
         } else {
           Status.login = LOGIN_FAILED_NETWORK_ERROR;
         }
         break;
     }
   },
 };
-
-
-/**
- * Send credentials over an active J-PAKE channel.
- * 
- * This object is designed to take over as the JPAKEClient controller,
- * presumably replacing one that is UI-based which would either cause
- * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
- * context disappears. This object stays alive for the duration of the
- * transfer by being strong-ref'ed as an nsIObserver.
- * 
- * Credentials are sent after the first sync has been completed
- * (successfully or not.)
- * 
- * Usage:
- * 
- *   jpakeclient.controller = new SendCredentialsController(jpakeclient);
- * 
- */
-function SendCredentialsController(jpakeclient) {
-  this._log = Log4Moz.repository.getLogger("Sync.SendCredentialsController");
-  this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
-
-  this._log.trace("Loading.");
-  this.jpakeclient = jpakeclient;
-
-  // Register ourselves as observers the first Sync finishing (either
-  // successfully or unsuccessfully, we don't care) or for removing
-  // this device's sync configuration, in case that happens while we
-  // haven't finished the first sync yet.
-  Services.obs.addObserver(this, "weave:service:sync:finish", false);
-  Services.obs.addObserver(this, "weave:service:sync:error",  false);
-  Services.obs.addObserver(this, "weave:service:start-over",  false);
-}
-SendCredentialsController.prototype = {
-
-  unload: function unload() {
-    this._log.trace("Unloading.");
-    try {
-      Services.obs.removeObserver(this, "weave:service:sync:finish");
-      Services.obs.removeObserver(this, "weave:service:sync:error");
-      Services.obs.removeObserver(this, "weave:service:start-over");
-    } catch (ex) {
-      // Ignore.
-    }
-  },
-
-  observe: function observe(subject, topic, data) {
-    switch (topic) {
-      case "weave:service:sync:finish":
-      case "weave:service:sync:error":
-        Utils.nextTick(this.sendCredentials, this);
-        break;
-      case "weave:service:start-over":
-        // This will call onAbort which will call unload().
-        this.jpakeclient.abort();
-        break;
-    }
-  },
-
-  sendCredentials: function sendCredentials() {
-    this._log.trace("Sending credentials.");
-    let credentials = {account:   Weave.Identity.account,
-                       password:  Weave.Identity.basicPassword,
-                       synckey:   Weave.Identity.syncKey,
-                       serverURL: Weave.Service.serverURL};
-    this.jpakeclient.sendAndComplete(credentials);
-  },
-
-  // JPAKEClient controller API
-
-  onComplete: function onComplete() {
-    this._log.debug("Exchange was completed successfully!");
-    this.unload();
-
-    // Schedule a Sync for soonish to fetch the data uploaded by the
-    // device with which we just paired.
-    SyncScheduler.scheduleNextSync(SyncScheduler.activeInterval);
-  },
-
-  onAbort: function onAbort(error) {
-    // It doesn't really matter why we aborted, but the channel is closed
-    // for sure, so we won't be able to do anything with it.
-    this._log.debug("Exchange was aborted with error: " + error);
-    this.unload();
-  },
-
-  // Irrelevant methods for this controller:
-  displayPIN: function displayPIN() {},
-  onPairingStart: function onPairingStart() {},
-  onPaired: function onPaired() {}
-};
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -1,27 +1,31 @@
 /* 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 = ["WBORecord", "RecordManager", "Records",
-                          "CryptoWrapper", "CollectionKeys", "Collection"];
+const EXPORTED_SYMBOLS = [
+  "WBORecord",
+  "RecordManager",
+  "CryptoWrapper",
+  "CollectionKeyManager",
+  "Collection",
+];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const CRYPTO_COLLECTION = "crypto";
 const KEYS_WBO = "keys";
 
+Cu.import("resource://services-common/log4moz.js");
 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-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.
@@ -32,27 +36,35 @@ WBORecord.prototype = {
   get sortindex() {
     if (this.data.sortindex)
       return this.data.sortindex;
     return 0;
   },
 
   // Get thyself from your URI, then deserialize.
   // Set thine 'response' field.
-  fetch: function fetch(uri) {
-    let r = new Resource(uri).get();
+  fetch: function fetch(resource) {
+    if (!resource instanceof Resource) {
+      throw new Error("First argument must be a Resource instance.");
+    }
+
+    let r = resource.get();
     if (r.success) {
       this.deserialize(r);   // Warning! Muffles exceptions!
     }
     this.response = r;
     return this;
   },
 
-  upload: function upload(uri) {
-    return new Resource(uri).put(this);
+  upload: function upload(resource) {
+    if (!resource instanceof Resource) {
+      throw new Error("First argument must be a Resource instance.");
+    }
+
+    return resource.put(this);
   },
 
   // Take a base URI string, with trailing slash, and return the URI of this
   // WBO based on collection and ID.
   uri: function(base) {
     if (this.collection && this.id) {
       let url = Utils.makeURI(base + this.collection + "/" + this.id);
       url.QueryInterface(Ci.nsIURL);
@@ -88,34 +100,35 @@ WBORecord.prototype = {
       "ttl: "      + this.ttl       + "  " +
       "payload: "  + JSON.stringify(this.payload) +
       " }";
   }
 };
 
 Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]);
 
-XPCOMUtils.defineLazyGetter(this, "Records", function () {
-  return new RecordManager();
-});
+/**
+ * An interface and caching layer for records.
+ */
+function RecordManager(service) {
+  this.service = service;
 
-function RecordManager() {
   this._log = Log4Moz.repository.getLogger(this._logName);
   this._records = {};
 }
 RecordManager.prototype = {
   _recordType: WBORecord,
   _logName: "Sync.RecordManager",
 
   import: function RecordMgr_import(url) {
     this._log.trace("Importing record: " + (url.spec ? url.spec : url));
     try {
       // Clear out the last response with empty object if GET fails
       this.response = {};
-      this.response = new Resource(url).get();
+      this.response = this.service.resource(url).get();
 
       // Don't parse and save the record on failure
       if (!this.response.success)
         return null;
 
       let record = new this._recordType(url);
       record.deserialize(this.response);
 
@@ -178,37 +191,35 @@ CryptoWrapper.prototype = {
    * collection, which is decrypted with the sync key.
    *
    * Cache those keys; invalidate the cache if the time on the keys collection
    * changes, or other auth events occur.
    *
    * Optional key bundle overrides the collection key lookup.
    */
   encrypt: function encrypt(keyBundle) {
-    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
     if (!keyBundle) {
-      throw new Error("Key bundle is null for " + this.uri.spec);
+      throw new Error("A key bundle must be supplied to encrypt.");
     }
 
     this.IV = Svc.Crypto.generateRandomIV();
     this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext),
                                          keyBundle.encryptionKeyB64, this.IV);
     this.hmac = this.ciphertextHMAC(keyBundle);
     this.cleartext = null;
   },
 
   // Optional key bundle.
   decrypt: function decrypt(keyBundle) {
     if (!this.ciphertext) {
       throw "No ciphertext: nothing to decrypt?";
     }
 
-    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
     if (!keyBundle) {
-      throw new Error("Key bundle is null for " + this.collection + "/" + this.id);
+      throw new Error("A key bundle must be supplied to decrypt.");
     }
 
     // Authenticate the encrypted blob with the expected HMAC
     let computedHMAC = this.ciphertextHMAC(keyBundle);
 
     if (computedHMAC != this.hmac) {
       Utils.throwHMACMismatch(this.hmac, computedHMAC);
     }
@@ -253,33 +264,29 @@ CryptoWrapper.prototype = {
     WBORecord.prototype.__lookupSetter__("id").call(this, val);
     return this.cleartext.id = val;
   },
 };
 
 Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
 Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
 
-XPCOMUtils.defineLazyGetter(this, "CollectionKeys", function () {
-  return new CollectionKeyManager();
-});
-
 
 /**
  * Keeps track of mappings between collection names ('tabs') and KeyBundles.
  *
  * You can update this thing simply by giving it /info/collections. It'll
  * use the last modified time to bring itself up to date.
  */
 function CollectionKeyManager() {
   this.lastModified = 0;
   this._collections = {};
   this._default = null;
 
-  this._log = Log4Moz.repository.getLogger("Sync.CollectionKeys");
+  this._log = Log4Moz.repository.getLogger("Sync.CollectionKeyManager");
 }
 
 // TODO: persist this locally as an Identity. Bug 610913.
 // Note that the last modified time needs to be preserved.
 CollectionKeyManager.prototype = {
 
   // Return information about old vs new keys:
   // * same: true if two collections are equal
@@ -308,17 +315,17 @@ CollectionKeyManager.prototype = {
             changed: changed};
   },
 
   get isClear() {
    return !this._default;
   },
 
   clear: function clear() {
-    this._log.info("Clearing CollectionKeys...");
+    this._log.info("Clearing collection keys...");
     this.lastModified = 0;
     this._collections = {};
     this._default = null;
   },
 
   keyForCollection: function(collection) {
     if (collection && this._collections[collection])
       return this._collections[collection];
@@ -411,26 +418,26 @@ CollectionKeyManager.prototype = {
   //
   setContents: function setContents(payload, modified) {
 
     if (!modified)
       throw "No modified time provided to setContents.";
 
     let self = this;
 
-    this._log.info("Setting CollectionKeys contents. Our last modified: " +
+    this._log.info("Setting collection keys contents. Our last modified: " +
                    this.lastModified + ", input modified: " + modified + ".");
 
     if (!payload)
-      throw "No payload in CollectionKeys.setContents().";
+      throw "No payload in CollectionKeyManager.setContents().";
 
     if (!payload.default) {
       this._log.warn("No downloaded default key: this should not occur.");
       this._log.warn("Not clearing local keys.");
-      throw "No default key in CollectionKeys.setContents(). Cannot proceed.";
+      throw "No default key in CollectionKeyManager.setContents(). Cannot proceed.";
     }
 
     // Process the incoming default key.
     let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
     b.keyPairB64 = payload.default;
     let newDefault = b;
 
     // Process the incoming collections.
@@ -493,19 +500,29 @@ CollectionKeyManager.prototype = {
     }
 
     let r = this.setContents(payload, storage_keys.modified);
     log.info("Collection keys updated.");
     return r;
   }
 }
 
-function Collection(uri, recordObj) {
+function Collection(uri, recordObj, service) {
+  if (!service) {
+    throw new Error("Collection constructor requires a service.");
+  }
+
   Resource.call(this, uri);
+
+  // This is a bit hacky, but gets the job done.
+  let res = service.resource(uri);
+  this.authenticator = res.authenticator;
+
   this._recordObj = recordObj;
+  this._service = service;
 
   this._full = false;
   this._ids = null;
   this._limit = 0;
   this._older = 0;
   this._newer = 0;
   this._data = [];
 }
@@ -603,10 +620,10 @@ Collection.prototype = {
         this._data = this._data.slice(newline + 1);
 
         // Deserialize a record from json and give it to the callback
         let record = new coll._recordObj();
         record.deserialize(json);
         onRecord(record);
       }
     };
-  }
+  },
 };
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -11,45 +11,44 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.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-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 const DEFAULT_LOAD_FLAGS =
   // Always validate the cache:
   Ci.nsIRequest.LOAD_BYPASS_CACHE |
   Ci.nsIRequest.INHIBIT_CACHING |
   // Don't send user cookies over the wire (Bug 644734).
   Ci.nsIRequest.LOAD_ANONYMOUS;
 
 /*
  * 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");
- * 
+ *
  * The 'resource' object has the following methods to issue HTTP requests
  * of the corresponding HTTP methods:
- * 
+ *
  *   get(callback)
  *   put(data, callback)
  *   post(data, callback)
  *   delete(callback)
- * 
+ *
  * 'callback' is a function with the following signature:
- * 
+ *
  *   function callback(error, result) {...}
- * 
+ *
  * 'error' will be null on successful requests. Likewise, result will not be
  * passed (=undefined) when an error occurs. Note that this is independent of
  * the status of the HTTP response.
  */
 function AsyncResource(uri) {
   this._log = Log4Moz.repository.getLogger(this._logName);
   this._log.level =
     Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
@@ -70,21 +69,21 @@ AsyncResource.prototype = {
    *
    * By default, a global authenticator is provided. If this is set, it will
    * be used instead of the global one.
    */
   authenticator: null,
 
   // The string to use as the base User-Agent in Sync requests.
   // These strings will look something like
-  // 
+  //
   //   Firefox/4.0 FxSync/1.8.0.20100101.mobile
-  //   
+  //
   // or
-  // 
+  //
   //   Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop
   //
   _userAgent:
     Services.appinfo.name + "/" + Services.appinfo.version +  // Product.
     " FxSync/" + WEAVE_VERSION + "." +                        // Sync.
     Services.appinfo.appBuildID + ".",                        // Build.
 
   // Wait 5 minutes before killing a request.
@@ -159,22 +158,18 @@ AsyncResource.prototype = {
     // Compose a UA string fragment from the various available identifiers.
     if (Svc.Prefs.get("sendVersionInfo", true)) {
       let ua = this._userAgent + Svc.Prefs.get("client.type", "desktop");
       channel.setRequestHeader("user-agent", ua, false);
     }
 
     let headers = this.headers;
 
-    let authenticator = this.authenticator;
-    if (!authenticator) {
-      authenticator = Identity.getResourceAuthenticator();
-    }
-    if (authenticator) {
-      let result = authenticator(this, method);
+    if (this.authenticator) {
+      let result = this.authenticator(this, method);
       if (result && result.headers) {
         for (let [k, v] in Iterator(result.headers)) {
           headers[k.toLowerCase()] = v;
         }
       }
     } else {
       this._log.debug("No authenticator found.");
     }
@@ -346,17 +341,17 @@ AsyncResource.prototype = {
     this._doRequest("DELETE", undefined, callback);
   }
 };
 
 
 /*
  * Represent a remote network resource, identified by a URI, with a
  * synchronous API.
- * 
+ *
  * 'Resource' is not recommended for new code. Use the asynchronous API of
  * 'AsyncResource' instead.
  */
 function Resource(uri) {
   AsyncResource.call(this, uri);
 }
 Resource.prototype = {
 
@@ -519,27 +514,27 @@ ChannelListener.prototype = {
     siStream.init(stream);
     try {
       this._data += siStream.read(count);
     } catch (ex) {
       this._log.warn("Exception thrown reading " + count +
                      " bytes from " + siStream + ".");
       throw ex;
     }
-    
+
     try {
       this._onProgress();
     } catch (ex) {
       this._log.warn("Got exception calling onProgress handler during fetch of "
                      + req.URI.spec);
       this._log.debug(Utils.exceptionStr(ex));
       this._log.trace("Rethrowing; expect a failure code from the HTTP channel.");
       throw ex;
     }
-    
+
     this.delayAbort();
   },
 
   /**
    * Create or push back the abort timer that kills this request
    */
   delayAbort: function delayAbort() {
     Utils.namedTimer(this.abortRequest, this._timeout, this, "abortTimer");
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -2,28 +2,29 @@
  * 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;
 
 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 = ["SyncStorageRequest"];
 
 const STORAGE_REQUEST_TIMEOUT = 5 * 60; // 5 minutes
 
 /**
  * RESTRequest variant for use against a Sync storage server.
  */
 function SyncStorageRequest(uri) {
   RESTRequest.call(this, uri);
+
+  this.authenticator = null;
 }
 SyncStorageRequest.prototype = {
 
   __proto__: RESTRequest.prototype,
 
   _logName: "Sync.StorageRequest",
 
   /**
@@ -48,19 +49,18 @@ SyncStorageRequest.prototype = {
 
   dispatch: function dispatch(method, data, onComplete, onProgress) {
     // Compose a UA string fragment from the various available identifiers.
     if (Svc.Prefs.get("sendVersionInfo", true)) {
       let ua = this.userAgent + Svc.Prefs.get("client.type", "desktop");
       this.setHeader("user-agent", ua);
     }
 
-    let authenticator = Identity.getRESTRequestAuthenticator();
-    if (authenticator) {
-      authenticator(this);
+    if (this.authenticator) {
+      this.authenticator(this);
     } else {
       this._log.debug("No authenticator found.");
     }
 
     return RESTRequest.prototype.dispatch.apply(this, arguments);
   },
 
   onStartRequest: function onStartRequest(channel) {
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -1,72 +1,82 @@
 /* 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/. */
 
-// 'Weave' continues to be exported for backwards compatibility.
-const EXPORTED_SYMBOLS = ["Service", "Weave"];
+const EXPORTED_SYMBOLS = ["Service"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 // How long before refreshing the cluster
 const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes
 
 // How long a key to generate from an old passphrase.
 const PBKDF2_KEY_BYTES = 16;
 
 const CRYPTO_COLLECTION = "crypto";
 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-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/utils.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-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-sync/stages/cluster.js");
+Cu.import("resource://services-sync/stages/enginesync.js");
 Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/userapi.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/main.js");
+
+const ENGINE_MODULES = {
+  Addons: "addons.js",
+  Apps: "apps.js",
+  Bookmarks: "bookmarks.js",
+  Form: "forms.js",
+  History: "history.js",
+  Password: "passwords.js",
+  Prefs: "prefs.js",
+  Tab: "tabs.js",
+};
 
 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
                             INFO_COLLECTION_USAGE,
                             INFO_COLLECTION_COUNTS,
                             INFO_QUOTA];
 
-/*
- * Service singleton
- * Main entry point into Weave's sync framework
- */
 
-function WeaveSvc() {
+function Sync11Service() {
   this._notify = Utils.notify("weave:service:");
 }
-WeaveSvc.prototype = {
+Sync11Service.prototype = {
 
   _lock: Utils.lock,
   _locked: false,
   _loggedIn: false,
-  _identity: Weave.Identity,
 
   userBaseURL: null,
   infoURL: null,
   storageURL: null,
   metaURL: null,
   cryptoKeyURL: null,
 
+  get enabledEngineNames() {
+    return [e.name for each (e in this.engineManager.getEnabled())];
+  },
+
   get serverURL() Svc.Prefs.get("serverURL"),
   set serverURL(value) {
     // Only do work if it's actually changing
     if (value == this.serverURL)
       return;
 
     // A new server most likely uses a different cluster, so clear that
     Svc.Prefs.set("serverURL", value);
@@ -82,22 +92,31 @@ WeaveSvc.prototype = {
   get miscAPI() {
     // Append to the serverURL if it's a relative fragment
     let misc = Svc.Prefs.get("miscURL");
     if (misc.indexOf(":") == -1)
       misc = this.serverURL + misc;
     return misc + MISC_API_VERSION + "/";
   },
 
-  get userAPI() {
-    // Append to the serverURL if it's a relative fragment
-    let user = Svc.Prefs.get("userURL");
-    if (user.indexOf(":") == -1)
-      user = this.serverURL + user;
-    return user + USER_API_VERSION + "/";
+  /**
+   * The URI of the User API service.
+   *
+   * This is the base URI of the service as applicable to all users up to
+   * and including the server version path component, complete with trailing
+   * forward slash.
+   */
+  get userAPIURI() {
+    // Append to the serverURL if it's a relative fragment.
+    let url = Svc.Prefs.get("userURL");
+    if (!url.contains(":")) {
+      url = this.serverURL + url;
+    }
+
+    return url + USER_API_VERSION + "/";
   },
 
   get pwResetURL() {
     return this.serverURL + "weave-password-reset";
   },
 
   get updatedURL() {
     return WEAVE_CHANNEL == "dev" ? UPDATED_DEV_URL : UPDATED_REL_URL;
@@ -136,21 +155,21 @@ WeaveSvc.prototype = {
       }
     }
 
     return Utils.catch.call(this, func, lockExceptions);
   },
 
   _updateCachedURLs: function _updateCachedURLs() {
     // Nothing to cache yet if we don't have the building blocks
-    if (this.clusterURL == "" || this._identity.username == "")
+    if (this.clusterURL == "" || this.identity.username == "")
       return;
 
     let storageAPI = this.clusterURL + SYNC_API_VERSION + "/";
-    this.userBaseURL = storageAPI + this._identity.username + "/";
+    this.userBaseURL = storageAPI + this.identity.username + "/";
     this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
 
     // Generate and cache various URLs under the storage API for this user
     this.infoURL = this.userBaseURL + "info/collections";
     this.storageURL = this.userBaseURL + "storage/";
     this.metaURL = this.storageURL + "meta/global";
     this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
   },
@@ -208,40 +227,40 @@ WeaveSvc.prototype = {
                    "or signaling to other clients.");
 
     // Set the last handled time so that we don't act again.
     this.lastHMACEvent = now;
 
     // Fetch keys.
     let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
     try {
-      let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
+      let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
 
       // Save out the ciphertext for when we reupload. If there's a bug in
-      // CollectionKeys, this will prevent us from uploading junk.
+      // CollectionKeyManager, this will prevent us from uploading junk.
       let cipherText = cryptoKeys.ciphertext;
 
       if (!cryptoResp.success) {
         this._log.warn("Failed to download keys.");
         return false;
       }
 
-      let keysChanged = this.handleFetchedKeys(this._identity.syncKeyBundle,
+      let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
                                                cryptoKeys, true);
       if (keysChanged) {
         // Did they change? If so, carry on.
         this._log.info("Suggesting retry.");
         return true;              // Try again.
       }
 
       // If not, reupload them and continue the current sync.
       cryptoKeys.ciphertext = cipherText;
       cryptoKeys.cleartext  = null;
 
-      let uploadResp = cryptoKeys.upload(this.cryptoKeysURL);
+      let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL));
       if (uploadResp.success)
         this._log.info("Successfully re-uploaded keys. Continuing sync.");
       else
         this._log.warn("Got error response re-uploading keys. " +
                        "Continuing sync; let's try again later.");
 
       return false;            // Don't try again: same keys.
 
@@ -249,20 +268,18 @@ WeaveSvc.prototype = {
       this._log.warn("Got exception \"" + ex + "\" fetching and handling " +
                      "crypto keys. Will try again later.");
       return false;
     }
   },
 
   handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
     // Don't want to wipe if we're just starting up!
-    // This is largely relevant because we don't persist
-    // CollectionKeys yet: Bug 610913.
-    let wasBlank = CollectionKeys.isClear;
-    let keysChanged = CollectionKeys.updateContents(syncKey, cryptoKeys);
+    let wasBlank = this.collectionKeys.isClear;
+    let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys);
 
     if (keysChanged && !wasBlank) {
       this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
 
       if (!skipReset) {
         this._log.info("Resetting client to reflect key change.");
 
         if (keysChanged.length) {
@@ -281,24 +298,40 @@ WeaveSvc.prototype = {
     return false;
   },
 
   /**
    * Prepare to initialize the rest of Weave after waiting a little bit
    */
   onStartup: function onStartup() {
     this._migratePrefs();
-    ErrorHandler.init();
+
+    // Status is instantiated before us and is the first to grab an instance of
+    // the IdentityManager. We use that instance because IdentityManager really
+    // needs to be a singleton. Ideally, the longer-lived object would spawn
+    // this service instance.
+    if (!Status || !Status._authManager) {
+      throw new Error("Status or Status._authManager not initialized.");
+    }
+
+    this.status = Status;
+    this.identity = Status._authManager;
+    this.collectionKeys = new CollectionKeyManager();
+
+    this.errorHandler = new ErrorHandler(this);
 
     this._log = Log4Moz.repository.getLogger("Sync.Service");
     this._log.level =
       Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
 
     this._log.info("Loading Weave " + WEAVE_VERSION);
 
+    this._clusterManager = new ClusterManager(this);
+    this.recordManager = new RecordManager(this);
+
     this.enabled = true;
 
     this._registerEngines();
 
     let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
       getService(Ci.nsIHttpProtocolHandler).userAgent;
     this._log.info(ua);
 
@@ -306,43 +339,43 @@ WeaveSvc.prototype = {
       this.enabled = false;
       this._log.info("Could not load the Weave crypto component. Disabling " +
                       "Weave, since it will not work correctly.");
     }
 
     Svc.Obs.add("weave:service:setup-complete", this);
     Svc.Prefs.observe("engine.", this);
 
-    SyncScheduler.init();
+    this.scheduler = new SyncScheduler(this);
 
     if (!this.enabled) {
       this._log.info("Firefox Sync disabled.");
     }
 
     this._updateCachedURLs();
 
     let status = this._checkSetup();
     if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
       Svc.Obs.notify("weave:engine:start-tracking");
     }
 
     // Send an event now that Weave service is ready.  We don't do this
     // synchronously so that observers can import this module before
     // registering an observer.
     Utils.nextTick(function onNextTick() {
-      Status.ready = true;
+      this.status.ready = true;
       Svc.Obs.notify("weave:service:ready");
-    });
+    }.bind(this));
   },
 
   _checkSetup: function _checkSetup() {
     if (!this.enabled) {
-      return Status.service = STATUS_DISABLED;
+      return this.status.service = STATUS_DISABLED;
     }
-    return Status.checkSetup();
+    return this.status.checkSetup();
   },
 
   _migratePrefs: function _migratePrefs() {
     // Migrate old debugLog prefs.
     let logLevel = Svc.Prefs.get("log.appender.debugLog");
     if (logLevel) {
       Svc.Prefs.set("log.appender.file.level", logLevel);
       Svc.Prefs.reset("log.appender.debugLog");
@@ -372,28 +405,50 @@ WeaveSvc.prototype = {
     oldPref.resetBranch("");
     Svc.Prefs.set("migrated", true);
   },
 
   /**
    * Register the built-in engines for certain applications
    */
   _registerEngines: function _registerEngines() {
+    this.engineManager = new EngineManager(this);
+
     let engines = [];
     // Applications can provide this preference (comma-separated list)
     // to specify which engines should be registered on startup.
     let pref = Svc.Prefs.get("registerEngines");
     if (pref) {
       engines = pref.split(",");
     }
 
-    // Grab the actual engines and register them
-    Engines.register(engines.map(function onItem(name) {
-      return Weave[name + "Engine"];
-    }));
+    this.clientsEngine = new ClientEngine(this);
+
+    for (let name of engines) {
+      if (!name in ENGINE_MODULES) {
+        this._log.info("Do not know about engine: " + name);
+        continue;
+      }
+
+      let ns = {};
+      try {
+        Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns);
+
+        let engineName = name + "Engine";
+        if (!(engineName in ns)) {
+          this._log.warn("Could not find exported engine instance: " + engineName);
+          continue;
+        }
+
+        this.engineManager.register(ns[engineName], this);
+      } catch (ex) {
+        this._log.warn("Could not register engine " + name + ": " +
+                       CommonUtils.exceptionStr(ex));
+      }
+    }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   // nsIObserver
 
   observe: function observe(subject, topic, data) {
@@ -418,187 +473,140 @@ WeaveSvc.prototype = {
       // The enabled status being changed back to what it was before.
       Svc.Prefs.reset("engineStatusChanged." + engine);
     } else {
       // Remember that the engine status changed locally until the next sync.
       Svc.Prefs.set("engineStatusChanged." + engine, true);
     }
   },
 
-  // gets cluster from central LDAP server and returns it, or null on error
-  _findCluster: function _findCluster() {
-    this._log.debug("Finding cluster for user " + this._identity.username);
+  /**
+   * Obtain a Resource instance with authentication credentials.
+   */
+  resource: function resource(url) {
+    let res = new Resource(url);
+    res.authenticator = this.identity.getResourceAuthenticator();
 
-    let fail;
-    let res = new Resource(this.userAPI + this._identity.username + "/node/weave");
-    try {
-      let node = res.get();
-      switch (node.status) {
-        case 400:
-          Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-          fail = "Find cluster denied: " + ErrorHandler.errorStr(node);
-          break;
-        case 404:
-          this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)");
-          return this.serverURL;
-        case 0:
-        case 200:
-          if (node == "null") {
-            node = null;
-          }
-          this._log.trace("_findCluster successfully returning " + node);
-          return node;
-        default:
-          ErrorHandler.checkServerError(node);
-          fail = "Unexpected response code: " + node.status;
-          break;
-      }
-    } catch (e) {
-      this._log.debug("Network error on findCluster");
-      Status.login = LOGIN_FAILED_NETWORK_ERROR;
-      ErrorHandler.checkServerError(e);
-      fail = e;
-    }
-    throw fail;
+    return res;
   },
 
-  // gets cluster from central LDAP server and sets this.clusterURL
-  _setCluster: function _setCluster() {
-    // Make sure we didn't get some unexpected response for the cluster
-    let cluster = this._findCluster();
-    this._log.debug("Cluster value = " + cluster);
-    if (cluster == null)
-      return false;
-
-    // Don't update stuff if we already have the right cluster
-    if (cluster == this.clusterURL)
-      return false;
+  /**
+   * Obtain a SyncStorageRequest instance with authentication credentials.
+   */
+  getStorageRequest: function getStorageRequest(url) {
+    let request = new SyncStorageRequest(url);
+    request.authenticator = this.identity.getRESTRequestAuthenticator();
 
-    this._log.debug("Setting cluster to " + cluster);
-    this.clusterURL = cluster;
-    Svc.Prefs.set("lastClusterUpdate", Date.now().toString());
-    return true;
-  },
-
-  // Update cluster if required.
-  // Returns false if the update was not required.
-  _updateCluster: function _updateCluster() {
-    this._log.info("Updating cluster.");
-    let cTime = Date.now();
-    let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate"));
-    if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) {
-      return this._setCluster();
-    }
-    return false;
+    return request;
   },
 
   /**
    * Perform the info fetch as part of a login or key fetch.
    */
   _fetchInfo: function _fetchInfo(url) {
     let infoURL = url || this.infoURL;
 
     this._log.trace("In _fetchInfo: " + infoURL);
     let info;
     try {
-      info = new Resource(infoURL).get();
+      info = this.resource(infoURL).get();
     } catch (ex) {
-      ErrorHandler.checkServerError(ex);
+      this.errorHandler.checkServerError(ex);
       throw ex;
     }
     if (!info.success) {
-      ErrorHandler.checkServerError(info);
+      this.errorHandler.checkServerError(info);
       throw "aborting sync, failed to get collections";
     }
     return info;
   },
 
   verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) {
 
     this._log.debug("Fetching and verifying -- or generating -- symmetric keys.");
 
     // Don't allow empty/missing passphrase.
     // Furthermore, we assume that our sync key is already upgraded,
     // and fail if that assumption is invalidated.
 
-    if (!this._identity.syncKey) {
-      Status.login = LOGIN_FAILED_NO_PASSPHRASE;
-      Status.sync = CREDENTIALS_CHANGED;
+    if (!this.identity.syncKey) {
+      this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+      this.status.sync = CREDENTIALS_CHANGED;
       return false;
     }
 
-    let syncKeyBundle = this._identity.syncKeyBundle;
+    let syncKeyBundle = this.identity.syncKeyBundle;
     if (!syncKeyBundle) {
       this._log.error("Sync Key Bundle not set. Invalid Sync Key?");
 
-      Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
-      Status.sync = CREDENTIALS_CHANGED;
+      this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
+      this.status.sync = CREDENTIALS_CHANGED;
       return false;
     }
 
     try {
       if (!infoResponse)
         infoResponse = this._fetchInfo();    // Will throw an exception on failure.
 
       // This only applies when the server is already at version 4.
       if (infoResponse.status != 200) {
         this._log.warn("info/collections returned non-200 response. Failing key fetch.");
-        Status.login = LOGIN_FAILED_SERVER_ERROR;
-        ErrorHandler.checkServerError(infoResponse);
+        this.status.login = LOGIN_FAILED_SERVER_ERROR;
+        this.errorHandler.checkServerError(infoResponse);
         return false;
       }
 
       let infoCollections = infoResponse.obj;
 
       this._log.info("Testing info/collections: " + JSON.stringify(infoCollections));
 
-      if (CollectionKeys.updateNeeded(infoCollections)) {
-        this._log.info("CollectionKeys reports that a key update is needed.");
+      if (this.collectionKeys.updateNeeded(infoCollections)) {
+        this._log.info("collection keys reports that a key update is needed.");
 
         // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
 
         // Fetch storage/crypto/keys.
         let cryptoKeys;
 
         if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) {
           try {
             cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
-            let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
+            let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
 
             if (cryptoResp.success) {
               let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys);
               return true;
             }
             else if (cryptoResp.status == 404) {
-              // On failure, ask CollectionKeys to generate new keys and upload them.
+              // On failure, ask to generate new keys and upload them.
               // Fall through to the behavior below.
               this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating.");
               cryptoKeys = null;
             }
             else {
               // Some other problem.
-              Status.login = LOGIN_FAILED_SERVER_ERROR;
-              ErrorHandler.checkServerError(cryptoResp);
+              this.status.login = LOGIN_FAILED_SERVER_ERROR;
+              this.errorHandler.checkServerError(cryptoResp);
               this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
               return false;
             }
           }
           catch (ex) {
             this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
             // TODO: Um, what exceptions might we get here? Should we re-throw any?
 
             // One kind of exception: HMAC failure.
             if (Utils.isHMACMismatch(ex)) {
-              Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
-              Status.sync = CREDENTIALS_CHANGED;
+              this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
+              this.status.sync = CREDENTIALS_CHANGED;
             }
             else {
               // In the absence of further disambiguation or more precise
               // failure constants, just report failure.
-              Status.login = LOGIN_FAILED;
+              this.status.login = LOGIN_FAILED;
             }
             return false;
           }
         }
         else {
           this._log.info("... 'crypto' is not a reported collection. Generating new keys.");
         }
 
@@ -620,121 +628,120 @@ WeaveSvc.prototype = {
         // No update needed: we're good!
         return true;
       }
 
     } catch (ex) {
       // This means no keys are present, or there's a network error.
       this._log.debug("Failed to fetch and verify keys: "
                       + Utils.exceptionStr(ex));
-      ErrorHandler.checkServerError(ex);
+      this.errorHandler.checkServerError(ex);
       return false;
     }
   },
 
-  verifyLogin: function verifyLogin()
-    this._notify("verify-login", "", function onNotify() {
-      if (!this._identity.username) {
-        this._log.warn("No username in verifyLogin.");
-        Status.login = LOGIN_FAILED_NO_USERNAME;
-        return false;
-      }
+  verifyLogin: function verifyLogin() {
+    if (!this.identity.username) {
+      this._log.warn("No username in verifyLogin.");
+      this.status.login = LOGIN_FAILED_NO_USERNAME;
+      return false;
+    }
 
-      // Unlock master password, or return.
-      // Attaching auth credentials to a request requires access to
-      // passwords, which means that Resource.get can throw MP-related
-      // exceptions!
-      // Try to fetch the passphrase first, while we still have control.
-      try {
-        this._identity.syncKey;
-      } catch (ex) {
-        this._log.debug("Fetching passphrase threw " + ex +
-                        "; assuming master password locked.");
-        Status.login = MASTER_PASSWORD_LOCKED;
-        return false;
+    // Unlock master password, or return.
+    // Attaching auth credentials to a request requires access to
+    // passwords, which means that Resource.get can throw MP-related
+    // exceptions!
+    // Try to fetch the passphrase first, while we still have control.
+    try {
+      this.identity.syncKey;
+    } catch (ex) {
+      this._log.debug("Fetching passphrase threw " + ex +
+                      "; assuming master password locked.");
+      this.status.login = MASTER_PASSWORD_LOCKED;
+      return false;
+    }
+
+    try {
+      // Make sure we have a cluster to verify against.
+      // This is a little weird, if we don't get a node we pretend
+      // to succeed, since that probably means we just don't have storage.
+      if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
+        this.status.sync = NO_SYNC_NODE_FOUND;
+        Svc.Obs.notify("weave:service:sync:delayed");
+        return true;
       }
 
-      try {
-        // Make sure we have a cluster to verify against.
-        // This is a little weird, if we don't get a node we pretend
-        // to succeed, since that probably means we just don't have storage.
-        if (this.clusterURL == "" && !this._setCluster()) {
-          Status.sync = NO_SYNC_NODE_FOUND;
-          Svc.Obs.notify("weave:service:sync:delayed");
-          return true;
-        }
+      // Fetch collection info on every startup.
+      let test = this.resource(this.infoURL).get();
 
-        // Fetch collection info on every startup.
-        let test = new Resource(this.infoURL).get();
+      switch (test.status) {
+        case 200:
+          // The user is authenticated.
 
-        switch (test.status) {
-          case 200:
-            // The user is authenticated.
+          // We have no way of verifying the passphrase right now,
+          // so wait until remoteSetup to do so.
+          // Just make the most trivial checks.
+          if (!this.identity.syncKey) {
+            this._log.warn("No passphrase in verifyLogin.");
+            this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+            return false;
+          }
 
-            // We have no way of verifying the passphrase right now,
-            // so wait until remoteSetup to do so.
-            // Just make the most trivial checks.
-            if (!this._identity.syncKey) {
-              this._log.warn("No passphrase in verifyLogin.");
-              Status.login = LOGIN_FAILED_NO_PASSPHRASE;
-              return false;
-            }
+          // Go ahead and do remote setup, so that we can determine
+          // conclusively that our passphrase is correct.
+          if (this._remoteSetup()) {
+            // Username/password verified.
+            this.status.login = LOGIN_SUCCEEDED;
+            return true;
+          }
+
+          this._log.warn("Remote setup failed.");
+          // Remote setup must have failed.
+          return false;
 
-            // Go ahead and do remote setup, so that we can determine
-            // conclusively that our passphrase is correct.
-            if (this._remoteSetup()) {
-              // Username/password verified.
-              Status.login = LOGIN_SUCCEEDED;
-              return true;
-            }
+        case 401:
+          this._log.warn("401: login failed.");
+          // Fall through to the 404 case.
 
-            this._log.warn("Remote setup failed.");
-            // Remote setup must have failed.
-            return false;
+        case 404:
+          // Check that we're verifying with the correct cluster
+          if (this._clusterManager.setCluster()) {
+            return this.verifyLogin();
+          }
 
-          case 401:
-            this._log.warn("401: login failed.");
-            // Fall through to the 404 case.
+          // We must have the right cluster, but the server doesn't expect us
+          this.status.login = LOGIN_FAILED_LOGIN_REJECTED;
+          return false;
 
-          case 404:
-            // Check that we're verifying with the correct cluster
-            if (this._setCluster())
-              return this.verifyLogin();
-
-            // We must have the right cluster, but the server doesn't expect us
-            Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-            return false;
-
-          default:
-            // Server didn't respond with something that we expected
-            Status.login = LOGIN_FAILED_SERVER_ERROR;
-            ErrorHandler.checkServerError(test);
-            return false;
-        }
+        default:
+          // Server didn't respond with something that we expected
+          this.status.login = LOGIN_FAILED_SERVER_ERROR;
+          this.errorHandler.checkServerError(test);
+          return false;
       }
-      catch (ex) {
-        // Must have failed on some network issue
-        this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
-        Status.login = LOGIN_FAILED_NETWORK_ERROR;
-        ErrorHandler.checkServerError(ex);
-        return false;
-      }
-    })(),
+    } catch (ex) {
+      // Must have failed on some network issue
+      this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
+      this.status.login = LOGIN_FAILED_NETWORK_ERROR;
+      this.errorHandler.checkServerError(ex);
+      return false;
+    }
+  },
 
   generateNewSymmetricKeys: function generateNewSymmetricKeys() {
     this._log.info("Generating new keys WBO...");
-    let wbo = CollectionKeys.generateNewKeysWBO();
+    let wbo = this.collectionKeys.generateNewKeysWBO();
     this._log.info("Encrypting new key bundle.");
-    wbo.encrypt(this._identity.syncKeyBundle);
+    wbo.encrypt(this.identity.syncKeyBundle);
 
     this._log.info("Uploading...");
-    let uploadRes = wbo.upload(this.cryptoKeysURL);
+    let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL));
     if (uploadRes.status != 200) {
       this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
-      ErrorHandler.checkServerError(uploadRes);
+      this.errorHandler.checkServerError(uploadRes);
       throw new Error("Unable to upload symmetric keys.");
     }
     this._log.info("Got status " + uploadRes.status + " uploading keys.");
     let serverModified = uploadRes.obj;   // Modified timestamp according to server.
     this._log.debug("Server reports crypto modified: " + serverModified);
 
     // Now verify that info/collections shows them!
     this._log.debug("Verifying server collection records.");
@@ -759,159 +766,159 @@ WeaveSvc.prototype = {
                       "is stale after successful upload.");
       throw new Error("Symmetric key upload failed.");
     }
     
     // Doesn't matter if the timestamp is ahead.
     
     // Download and install them.
     let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
-    let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
+    let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
     if (cryptoResp.status != 200) {
       this._log.warn("Failed to download keys.");
       throw new Error("Symmetric key download failed.");
     }
-    let keysChanged = this.handleFetchedKeys(this._identity.syncKeyBundle,
+    let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
                                              cryptoKeys, true);
     if (keysChanged) {
       this._log.info("Downloaded keys differed, as expected.");
     }
   },
 
-  changePassword: function changePassword(newpass) {
-    return this._notify("changepwd", "", function onNotify() {
-      let url = this.userAPI + this._identity.username + "/password";
-      try {
-        let resp = new Resource(url).post(Utils.encodeUTF8(newpass));
-        if (resp.status != 200) {
-          this._log.debug("Password change failed: " + resp);
-          return false;
-        }
-      }
-      catch(ex) {
-        // Must have failed on some network issue
-        this._log.debug("changePassword failed: " + Utils.exceptionStr(ex));
-        return false;
-      }
+  changePassword: function changePassword(newPassword) {
+    let client = new UserAPI10Client(this.userAPIURI);
+    let cb = Async.makeSpinningCallback();
+    client.changePassword(this.identity.username,
+                          this.identity.basicPassword, newPassword, cb);
 
-      // Save the new password for requests and login manager.
-      this._identity.basicPassword = newpass;
-      this.persistLogin();
-      return true;
-    })();
+    try {
+      cb.wait();
+    } catch (ex) {
+      this._log.debug("Password change failed: " +
+                      CommonUtils.exceptionStr(ex));
+      return false;
+    }
+
+    // Save the new password for requests and login manager.
+    this.identity.basicPassword = newPassword;
+    this.persistLogin();
+    return true;
   },
 
   changePassphrase: function changePassphrase(newphrase) {
-    return this._catch(this._notify("changepph", "", function onNotify() {
+    return this._catch(function doChangePasphrase() {
       /* Wipe. */
       this.wipeServer();
 
       this.logout();
 
       /* Set this so UI is updated on next run. */
-      this._identity.syncKey = newphrase;
+      this.identity.syncKey = newphrase;
       this.persistLogin();
 
       /* We need to re-encrypt everything, so reset. */
       this.resetClient();
-      CollectionKeys.clear();
+      this.collectionKeys.clear();
 
       /* Login and sync. This also generates new keys. */
       this.sync();
+
+      Svc.Obs.notify("weave:service:change-passphrase", true);
+
       return true;
-    }))();
+    })();
   },
 
   startOver: function startOver() {
     this._log.trace("Invoking Service.startOver.");
     Svc.Obs.notify("weave:engine:stop-tracking");
-    Status.resetSync();
+    this.status.resetSync();
 
     // We want let UI consumers of the following notification know as soon as
     // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
     // by emptying the passphrase (we still need the password).
-    this._identity.syncKey = null;
-    Status.login = LOGIN_FAILED_NO_PASSPHRASE;
+    this.identity.syncKey = null;
+    this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
     this.logout();
     Svc.Obs.notify("weave:service:start-over");
 
     // Deletion doesn't make sense if we aren't set up yet!
     if (this.clusterURL != "") {
       // Clear client-specific data from the server, including disabled engines.
-      for each (let engine in [Clients].concat(Engines.getAll())) {
+      for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) {
         try {
           engine.removeClientData();
         } catch(ex) {
           this._log.warn("Deleting client data for " + engine.name + " failed:"
                          + Utils.exceptionStr(ex));
         }
       }
     } else {
       this._log.debug("Skipping client data removal: no cluster URL.");
     }
 
     // Reset all engines and clear keys.
     this.resetClient();
-    CollectionKeys.clear();
-    Status.resetBackoff();
+    this.collectionKeys.clear();
+    this.status.resetBackoff();
 
     // Reset Weave prefs.
     this._ignorePrefObserver = true;
     Svc.Prefs.resetBranch("");
     this._ignorePrefObserver = false;
 
     Svc.Prefs.set("lastversion", WEAVE_VERSION);
 
-    this._identity.deleteSyncCredentials();
+    this.identity.deleteSyncCredentials();
   },
 
   persistLogin: function persistLogin() {
     try {
-      this._identity.persistCredentials(true);
+      this.identity.persistCredentials(true);
     } catch (ex) {
       this._log.info("Unable to persist credentials: " + ex);
     }
   },
 
   login: function login(username, password, passphrase) {
     function onNotify() {
       this._loggedIn = false;
       if (Services.io.offline) {
-        Status.login = LOGIN_FAILED_NETWORK_ERROR;
+        this.status.login = LOGIN_FAILED_NETWORK_ERROR;
         throw "Application is offline, login should not be called";
       }
 
       let initialStatus = this._checkSetup();
       if (username) {
-        this._identity.username = username;
+        this.identity.username = username;
       }
       if (password) {
-        this._identity.basicPassword = password;
+        this.identity.basicPassword = password;
       }
       if (passphrase) {
-        this._identity.syncKey = passphrase;
+        this.identity.syncKey = passphrase;
       }
 
       if (this._checkSetup() == CLIENT_NOT_CONFIGURED) {
         throw "Aborting login, client not configured.";
       }
 
       // Calling login() with parameters when the client was
       // previously not configured means setup was completed.
       if (initialStatus == CLIENT_NOT_CONFIGURED
           && (username || password || passphrase)) {
         Svc.Obs.notify("weave:service:setup-complete");
       }
 
-      this._log.info("Logging in user " + this._identity.username);
+      this._log.info("Logging in user " + this.identity.username);
       this._updateCachedURLs();
 
       if (!this.verifyLogin()) {
         // verifyLogin sets the failure states here.
-        throw "Login failed: " + Status.login;
+        throw "Login failed: " + this.status.login;
       }
 
       this._loggedIn = true;
 
       return true;
     }
 
     let notifier = this._notify("login", "", onNotify.bind(this));
@@ -925,107 +932,87 @@ WeaveSvc.prototype = {
 
     this._log.info("Logging out");
     this._loggedIn = false;
 
     Svc.Obs.notify("weave:service:logout:finish");
   },
 
   checkAccount: function checkAccount(account) {
-    let username = this._identity.usernameFromAccount(account);
-    let url = this.userAPI + username;
-    let res = new Resource(url);
+    let client = new UserAPI10Client(this.userAPIURI);
+    let cb = Async.makeSpinningCallback();
 
-    let data = "";
+    let username = this.identity.usernameFromAccount(account);
+    client.usernameExists(username, cb);
+
     try {
-      data = res.get();
-      if (data.status == 200) {
-        if (data == "0")
-          return "available";
-        else if (data == "1")
-          return "notAvailable";
-      }
-
+      let exists = cb.wait();
+      return exists ? "notAvailable" : "available";
+    } catch (ex) {
+      // TODO fix API convention.
+      return this.errorHandler.errorStr(ex);
     }
-    catch(ex) {}
-
-    // Convert to the error string, or default to generic on exception.
-    return ErrorHandler.errorStr(data);
   },
 
   createAccount: function createAccount(email, password,
                                         captchaChallenge, captchaResponse) {
-    let username = this._identity.usernameFromAccount(email);
-    let payload = JSON.stringify({
-      "password": Utils.encodeUTF8(password),
-      "email": email,
-      "captcha-challenge": captchaChallenge,
-      "captcha-response": captchaResponse
-    });
-
-    let url = this.userAPI + username;
-    let res = new Resource(url);
+    let client = new UserAPI10Client(this.userAPIURI);
 
     // Hint to server to allow scripted user creation or otherwise
     // ignore captcha.
-    if (Svc.Prefs.isSet("admin-secret"))
-      res.setHeader("X-Weave-Secret", Svc.Prefs.get("admin-secret", ""));
-
-    let error = "generic-server-error";
-    try {
-      let register = res.put(payload);
-      if (register.success) {
-        this._log.info("Account created: " + register);
-        return null;
-      }
-
-      // Must have failed, so figure out the reason
-      if (register.status == 400)
-        error = ErrorHandler.errorStr(register);
-    }
-    catch(ex) {
-      this._log.warn("Failed to create account: " + ex);
+    if (Svc.Prefs.isSet("admin-secret")) {
+      client.adminSecret = Svc.Prefs.get("admin-secret", "");
     }
 
-    return error;
+    let cb = Async.makeSpinningCallback();
+
+    client.createAccount(email, password, captchaChallenge, captchaResponse,
+                         cb);
+
+    try {
+      cb.wait();
+      return null;
+    } catch (ex) {
+      return this.errorHandler.errorStr(ex.body);
+    }
   },
 
   // Stuff we need to do after login, before we can really do
   // anything (e.g. key setup).
   _remoteSetup: function _remoteSetup(infoResponse) {
     let reset = false;
 
     this._log.debug("Fetching global metadata record");
-    let meta = Records.get(this.metaURL);
+    let meta = this.recordManager.get(this.metaURL);
 
     // Checking modified time of the meta record.
     if (infoResponse &&
         (infoResponse.obj.meta != this.metaModified) &&
         (!meta || !meta.isNew)) {
 
       // Delete the cached meta record...
       this._log.debug("Clearing cached meta record. metaModified is " +
           JSON.stringify(this.metaModified) + ", setting to " +
           JSON.stringify(infoResponse.obj.meta));
 
-      Records.del(this.metaURL);
+      this.recordManager.del(this.metaURL);
 
       // ... fetch the current record from the server, and COPY THE FLAGS.
-      let newMeta = Records.get(this.metaURL);
+      let newMeta = this.recordManager.get(this.metaURL);
 
-      if (!Records.response.success || !newMeta) {
+      if (!this.recordManager.response.success || !newMeta) {
         this._log.debug("No meta/global record on the server. Creating one.");
         newMeta = new WBORecord("meta", "global");
         newMeta.payload.syncID = this.syncID;
         newMeta.payload.storageVersion = STORAGE_VERSION;
 
         newMeta.isNew = true;
 
-        Records.set(this.metaURL, newMeta);
-        if (!newMeta.upload(this.metaURL).success) {
+        this.recordManager.set(this.metaURL, newMeta);
+        if (!newMeta.upload(this.resource(this.metaURL)).success) {
           this._log.warn("Unable to upload new meta/global. Failing remote setup.");
           return false;
         }
       } else {
         // If newMeta, then it stands to reason that meta != null.
         newMeta.isNew   = meta.isNew;
         newMeta.changed = meta.changed;
       }
@@ -1044,20 +1031,20 @@ WeaveSvc.prototype = {
     // Check for cases that require a fresh start. When comparing remoteVersion,
     // we need to convert it to a number as older clients used it as a string.
     if (!meta || !meta.payload.storageVersion || !meta.payload.syncID ||
         STORAGE_VERSION > parseFloat(remoteVersion)) {
 
       this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed.");
 
       // abort the server wipe if the GET status was anything other than 404 or 200
-      let status = Records.response.status;
+      let status = this.recordManager.response.status;
       if (status != 200 && status != 404) {
-        Status.sync = METARECORD_DOWNLOAD_FAIL;
-        ErrorHandler.checkServerError(Records.response);
+        this.status.sync = METARECORD_DOWNLOAD_FAIL;
+        this.errorHandler.checkServerError(this.recordManager.response);
         this._log.warn("Unknown error while downloading metadata record. " +
                        "Aborting sync.");
         return false;
       }
 
       if (!meta)
         this._log.info("No metadata record, server wipe needed");
       if (meta && !meta.payload.syncID)
@@ -1072,41 +1059,41 @@ WeaveSvc.prototype = {
         this._log.info("Metadata record not found, server was wiped to ensure " +
                        "consistency.");
       else // 200
         this._log.info("Wiped server; incompatible metadata: " + remoteVersion);
 
       return true;
     }
     else if (remoteVersion > STORAGE_VERSION) {
-      Status.sync = VERSION_OUT_OF_DATE;
+      this.status.sync = VERSION_OUT_OF_DATE;
       this._log.warn("Upgrade required to access newer storage version.");
       return false;
     }
     else if (meta.payload.syncID != this.syncID) {
 
       this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID);
       this.resetClient();
-      CollectionKeys.clear();
+      this.collectionKeys.clear();
       this.syncID = meta.payload.syncID;
       this._log.debug("Clear cached values and take syncId: " + this.syncID);
 
       if (!this.upgradeSyncKey(meta.payload.syncID)) {
         this._log.warn("Failed to upgrade sync key. Failing remote setup.");
         return false;
       }
 
       if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
         this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
         return false;
       }
 
       // bug 545725 - re-verify creds and fail sanely
       if (!this.verifyLogin()) {
-        Status.sync = CREDENTIALS_CHANGED;
+        this.status.sync = CREDENTIALS_CHANGED;
         this._log.info("Credentials have changed, aborting sync and forcing re-login.");
         return false;
       }
 
       return true;
     }
     else {
       if (!this.upgradeSyncKey(meta.payload.syncID)) {
@@ -1145,19 +1132,19 @@ WeaveSvc.prototype = {
    * @return Reason for not syncing; not-truthy if sync should run
    */
   _checkSync: function _checkSync(ignore) {
     let reason = "";
     if (!this.enabled)
       reason = kSyncWeaveDisabled;
     else if (Services.io.offline)
       reason = kSyncNetworkOffline;
-    else if (Status.minimumNextSync > Date.now())
+    else if (this.status.minimumNextSync > Date.now())
       reason = kSyncBackoffNotMet;
-    else if ((Status.login == MASTER_PASSWORD_LOCKED) &&
+    else if ((this.status.login == MASTER_PASSWORD_LOCKED) &&
              Utils.mpLocked())
       reason = kSyncMasterPasswordLocked;
     else if (Svc.Prefs.get("firstSync") == "notReady")
       reason = kFirstSyncChoiceNotMade;
 
     if (ignore && ignore.indexOf(reason) != -1)
       return "";
 
@@ -1182,252 +1169,43 @@ WeaveSvc.prototype = {
       }
       return this._lockedSync.apply(this, arguments);
     })();
   },
 
   /**
    * Sync up engines with the server.
    */
-  _lockedSync: function _lockedSync()
-    this._lock("service.js: sync",
-               this._notify("sync", "", function onNotify() {
-
-    this._log.info("In sync().");
-
-    let syncStartTime = Date.now();
-
-    Status.resetSync();
-
-    // Make sure we should sync or record why we shouldn't
-    let reason = this._checkSync();
-    if (reason) {
-      if (reason == kSyncNetworkOffline) {
-        Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-      }
-      // this is a purposeful abort rather than a failure, so don't set
-      // any status bits
-      reason = "Can't sync: " + reason;
-      throw reason;
-    }
-
-    // if we don't have a node, get one.  if that fails, retry in 10 minutes
-    if (this.clusterURL == "" && !this._setCluster()) {
-      Status.sync = NO_SYNC_NODE_FOUND;
-      return;
-    }
-
-    // Ping the server with a special info request once a day.
-    let infoURL = this.infoURL;
-    let now = Math.floor(Date.now() / 1000);
-    let lastPing = Svc.Prefs.get("lastPing", 0);
-    if (now - lastPing > 86400) { // 60 * 60 * 24
-      infoURL += "?v=" + WEAVE_VERSION;
-      Svc.Prefs.set("lastPing", now);
-    }
-
-    // Figure out what the last modified time is for each collection
-    let info = this._fetchInfo(infoURL);
-
-    // Convert the response to an object and read out the modified times
-    for each (let engine in [Clients].concat(Engines.getAll()))
-      engine.lastModified = info.obj[engine.name] || 0;
-
-    if (!(this._remoteSetup(info)))
-      throw "aborting sync, remote setup failed";
-
-    // Make sure we have an up-to-date list of clients before sending commands
-    this._log.debug("Refreshing client list.");
-    if (!this._syncEngine(Clients)) {
-      // Clients is an engine like any other; it can fail with a 401,
-      // and we can elect to abort the sync.
-      this._log.warn("Client engine sync failed. Aborting.");
-      return;
-    }
-
-    // Wipe data in the desired direction if necessary
-    switch (Svc.Prefs.get("firstSync")) {
-      case "resetClient":
-        this.resetClient(Engines.getEnabled().map(function(e) e.name));
-        break;
-      case "wipeClient":
-        this.wipeClient(Engines.getEnabled().map(function(e) e.name));
-        break;
-      case "wipeRemote":
-        this.wipeRemote(Engines.getEnabled().map(function(e) e.name));
-        break;
-    }
-
-    if (Clients.localCommands) {
-      try {
-        if (!(Clients.processIncomingCommands())) {
-          Status.sync = ABORT_SYNC_COMMAND;
-          throw "aborting sync, process commands said so";
-        }
-
-        // Repeat remoteSetup in-case the commands forced us to reset
-        if (!(this._remoteSetup(info)))
-          throw "aborting sync, remote setup failed after processing commands";
-      }
-      finally {
-        // Always immediately attempt to push back the local client (now
-        // without commands).
-        // Note that we don't abort here; if there's a 401 because we've
-        // been reassigned, we'll handle it around another engine.
-        this._syncEngine(Clients);
-      }
-    }
-
-    // Update engines because it might change what we sync.
-    try {
-      this._updateEnabledEngines();
-    } catch (ex) {
-      this._log.debug("Updating enabled engines failed: " +
-                      Utils.exceptionStr(ex));
-      ErrorHandler.checkServerError(ex);
-      throw ex;
-    }
-
-    try {
-      for each (let engine in Engines.getEnabled()) {
-        // If there's any problems with syncing the engine, report the failure
-        if (!(this._syncEngine(engine)) || Status.enforceBackoff) {
-          this._log.info("Aborting sync");
-          break;
-        }
-      }
+  _lockedSync: function _lockedSync() {
+    return this._lock("service.js: sync",
+                      this._notify("sync", "", function onNotify() {
 
-      // If _syncEngine fails for a 401, we might not have a cluster URL here.
-      // If that's the case, break out of this immediately, rather than
-      // throwing an exception when trying to fetch metaURL.
-      if (!this.clusterURL) {
-        this._log.debug("Aborting sync, no cluster URL: " +
-                        "not uploading new meta/global.");
-        return;
-      }
-
-      // Upload meta/global if any engines changed anything
-      let meta = Records.get(this.metaURL);
-      if (meta.isNew || meta.changed) {
-        new Resource(this.metaURL).put(meta);
-        delete meta.isNew;
-        delete meta.changed;
-      }
-
-      // If there were no sync engine failures
-      if (Status.service != SYNC_FAILED_PARTIAL) {
-        Svc.Prefs.set("lastSync", new Date().toString());
-        Status.sync = SYNC_SUCCEEDED;
-      }
-    } finally {
-      Svc.Prefs.reset("firstSync");
-
-      let syncTime = ((Date.now() - syncStartTime) / 1000).toFixed(2);
-      let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
-      this._log.info("Sync completed at " + dateStr
-                     + " after " + syncTime + " secs.");
-    }
-  }))(),
-
-
-  _updateEnabledEngines: function _updateEnabledEngines() {
-    this._log.info("Updating enabled engines: " + SyncScheduler.numClients + " clients.");
-    let meta = Records.get(this.metaURL);
-    if (meta.isNew || !meta.payload.engines)
-      return;
-
-    // If we're the only client, and no engines are marked as enabled,
-    // thumb our noses at the server data: it can't be right.
-    // Belt-and-suspenders approach to Bug 615926.
-    if ((SyncScheduler.numClients <= 1) &&
-        ([e for (e in meta.payload.engines) if (e != "clients")].length == 0)) {
-      this._log.info("One client and no enabled engines: not touching local engine status.");
-      return;
-    }
-
-    this._ignorePrefObserver = true;
+      let synchronizer = new EngineSynchronizer(this);
+      let cb = Async.makeSpinningCallback();
+      synchronizer.onComplete = cb;
 
-    let enabled = [eng.name for each (eng in Engines.getEnabled())];
-    for (let engineName in meta.payload.engines) {
-      if (engineName == "clients") {
-        // Clients is special.
-        continue;
-      }
-      let index = enabled.indexOf(engineName);
-      if (index != -1) {
-        // The engine is enabled locally. Nothing to do.
-        enabled.splice(index, 1);
-        continue;
-      }
-      let engine = Engines.get(engineName);
-      if (!engine) {
-        // The engine doesn't exist locally. Nothing to do.
-        continue;
-      }
-
-      if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
-        // The engine was disabled locally. Wipe server data and
-        // disable it everywhere.
-        this._log.trace("Wiping data for " + engineName + " engine.");
-        engine.wipeServer();
-        delete meta.payload.engines[engineName];
-        meta.changed = true;
-      } else {
-        // The engine was enabled remotely. Enable it locally.
-        this._log.trace(engineName + " engine was enabled remotely.");
-        engine.enabled = true;
-      }
-    }
-
-    // Any remaining engines were either enabled locally or disabled remotely.
-    for each (let engineName in enabled) {
-      let engine = Engines.get(engineName);
-      if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
-        this._log.trace("The " + engineName + " engine was enabled locally.");
-      } else {
-        this._log.trace("The " + engineName + " engine was disabled remotely.");
-        engine.enabled = false;
-      }
-    }
-
-    Svc.Prefs.resetBranch("engineStatusChanged.");
-    this._ignorePrefObserver = false;
-  },
-
-  // Returns true if sync should proceed.
-  // false / no return value means sync should be aborted.
-  _syncEngine: function _syncEngine(engine) {
-    try {
-      engine.sync();
-    }
-    catch(e) {
-      if (e.status == 401) {
-        // Maybe a 401, cluster update perhaps needed?
-        // We rely on ErrorHandler observing the sync failure notification to
-        // schedule another sync and clear node assignment values.
-        // Here we simply want to muffle the exception and return an
-        // appropriate value.
-        return false;
-      }
-    }
-    return true;
+      synchronizer.sync();
+      // wait() throws if the first argument is truthy, which is exactly what
+      // we want.
+      let result = cb.wait();
+    }))();
   },
 
   /**
    * If we have a passphrase, rather than a 25-alphadigit sync key,
    * use the provided sync ID to bootstrap it using PBKDF2.
    *
    * Store the new 'passphrase' back into the identity manager.
    *
    * We can check this as often as we want, because once it's done the
    * check will no longer succeed. It only matters that it happens after
    * we decide to bump the server storage version.
    */
   upgradeSyncKey: function upgradeSyncKey(syncID) {
-    let p = this._identity.syncKey;
+    let p = this.identity.syncKey;
 
     if (!p) {
       return false;
     }
 
     // Check whether it's already a key that we generated.
     if (Utils.isPassphrase(p)) {
       this._log.info("Sync key is up-to-date: no need to upgrade.");
@@ -1441,141 +1219,145 @@ WeaveSvc.prototype = {
     let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES);   // Base 32.
 
     if (!k) {
       this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade.");
       return false;
     }
 
     this._log.info("Upgrading sync key...");
-    this._identity.syncKey = k;
+    this.identity.syncKey = k;
     this._log.info("Saving upgraded sync key...");
     this.persistLogin();
     this._log.info("Done saving.");
     return true;
   },
 
   _freshStart: function _freshStart() {
     this._log.info("Fresh start. Resetting client and considering key upgrade.");
     this.resetClient();
-    CollectionKeys.clear();
+    this.collectionKeys.clear();
     this.upgradeSyncKey(this.syncID);
 
     // Wipe the server.
     let wipeTimestamp = this.wipeServer();
 
     // Upload a new meta/global record.
     let meta = new WBORecord("meta", "global");
     meta.payload.syncID = this.syncID;
     meta.payload.storageVersion = STORAGE_VERSION;
     meta.isNew = true;
 
     this._log.debug("New metadata record: " + JSON.stringify(meta.payload));
-    let res = new Resource(this.metaURL);
+    let res = this.resource(this.metaURL);
     // It would be good to set the X-If-Unmodified-Since header to `timestamp`
     // for this PUT to ensure at least some level of transactionality.
     // Unfortunately, the servers don't support it after a wipe right now
     // (bug 693893), so we're going to defer this until bug 692700.
     let resp = res.put(meta);
     if (!resp.success) {
       // If we got into a race condition, we'll abort the sync this way, too.
       // That's fine. We'll just wait till the next sync. The client that we're
       // racing is probably busy uploading stuff right now anyway.
       throw resp;
     }
-    Records.set(this.metaURL, meta);
+    this.recordManager.set(this.metaURL, meta);
 
     // Wipe everything we know about except meta because we just uploaded it
-    let collections = [Clients].concat(Engines.getAll()).map(function(engine) {
-      return engine.name;
-    });
+    let engines = [this.clientsEngine].concat(this.engineManager.getAll());
+    let collections = [engine.name for each (engine in engines)];
 
     // Generate, upload, and download new keys. Do this last so we don't wipe
     // them...
     this.generateNewSymmetricKeys();
   },
 
   /**
    * Wipe user data from the server.
    *
    * @param collections [optional]
    *        Array of collections to wipe. If not given, all collections are
    *        wiped by issuing a DELETE request for `storageURL`.
    *
    * @return the server's timestamp of the (last) DELETE.
    */
-  wipeServer: function wipeServer(collections)
-    this._notify("wipe-server", "", function onNotify() {
-      let response;
-      if (!collections) {
-        // Strip the trailing slash.
-        let res = new Resource(this.storageURL.slice(0, -1));
-        res.setHeader("X-Confirm-Delete", "1");
-        try {
-          response = res.delete();
-        } catch (ex) {
-          this._log.debug("Failed to wipe server: " + Utils.exceptionStr(ex));
-          throw ex;
-        }
-        if (response.status != 200 && response.status != 404) {
-          this._log.debug("Aborting wipeServer. Server responded with " +
-                          response.status + " response for " + this.storageURL);
-          throw response;
-        }
-        return response.headers["x-weave-timestamp"];
+  wipeServer: function wipeServer(collections) {
+    let response;
+    if (!collections) {
+      // Strip the trailing slash.
+      let res = this.resource(this.storageURL.slice(0, -1));
+      res.setHeader("X-Confirm-Delete", "1");
+      try {
+        response = res.delete();
+      } catch (ex) {
+        this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex));
+        throw ex;
+      }
+      if (response.status != 200 && response.status != 404) {
+        this._log.debug("Aborting wipeServer. Server responded with " +
+                        response.status + " response for " + this.storageURL);
+        throw response;
       }
-      let timestamp;
-      for each (let name in collections) {
-        let url = this.storageURL + name;
-        try {
-          response = new Resource(url).delete();
-        } catch (ex) {
-          this._log.debug("Failed to wipe '" + name + "' collection: " +
-                          Utils.exceptionStr(ex));
-          throw ex;
-        }
-        if (response.status != 200 && response.status != 404) {
-          this._log.debug("Aborting wipeServer. Server responded with " +
-                          response.status + " response for " + url);
-          throw response;
-        }
-        if ("x-weave-timestamp" in response.headers) {
-          timestamp = response.headers["x-weave-timestamp"];
-        }
+      return response.headers["x-weave-timestamp"];
+    }
+
+    let timestamp;
+    for (let name of collections) {
+      let url = this.storageURL + name;
+      try {
+        response = this.resource(url).delete();
+      } catch (ex) {
+        this._log.debug("Failed to wipe '" + name + "' collection: " +
+                        Utils.exceptionStr(ex));
+        throw ex;
       }
-      return timestamp;
-    })(),
+
+      if (response.status != 200 && response.status != 404) {
+        this._log.debug("Aborting wipeServer. Server responded with " +
+                        response.status + " response for " + url);
+        throw response;
+      }
+
+      if ("x-weave-timestamp" in response.headers) {
+        timestamp = response.headers["x-weave-timestamp"];
+      }
+    }
+
+    return timestamp;
+  },
 
   /**
    * Wipe all local user data.
    *
    * @param engines [optional]
    *        Array of engine names to wipe. If not given, all engines are used.
    */
-  wipeClient: function wipeClient(engines)
-    this._notify("wipe-client", "", function onNotify() {
-      // If we don't have any engines, reset the service and wipe all engines
-      if (!engines) {
-        // Clear out any service data
-        this.resetService();
+  wipeClient: function wipeClient(engines) {
+    // If we don't have any engines, reset the service and wipe all engines
+    if (!engines) {
+      // Clear out any service data
+      this.resetService();
 
-        engines = [Clients].concat(Engines.getAll());
+      engines = [this.clientsEngine].concat(this.engineManager.getAll());
+    }
+    // Convert the array of names into engines
+    else {
+      engines = this.engineManager.get(engines);
+    }
+
+    // Fully wipe each engine if it's able to decrypt data
+    for each (let engine in engines) {
+      if (engine.canDecrypt()) {
+        engine.wipeClient();
       }
-      // Convert the array of names into engines
-      else
-        engines = Engines.get(engines);
+    }
 
-      // Fully wipe each engine if it's able to decrypt data
-      for each (let engine in engines)
-        if (engine.canDecrypt())
-          engine.wipeClient();
-
-      // Save the password/passphrase just in-case they aren't restored by sync
-      this.persistLogin();
-    })(),
+    // Save the password/passphrase just in-case they aren't restored by sync
+    this.persistLogin();
+  },
 
   /**
    * Wipe all remote user data by wiping the server then telling each remote
    * client to wipe itself.
    *
    * @param engines [optional]
    *        Array of engine names to wipe. If not given, all engines are used.
    */
@@ -1584,66 +1366,70 @@ WeaveSvc.prototype = {
       // Make sure stuff gets uploaded.
       this.resetClient(engines);
 
       // Clear out any server data.
       this.wipeServer(engines);
 
       // Only wipe the engines provided.
       if (engines) {
-        engines.forEach(function(e) Clients.sendCommand("wipeEngine", [e]), this);
+        engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this);
       }
       // Tell the remote machines to wipe themselves.
       else {
-        Clients.sendCommand("wipeAll", []);
+        this.clientsEngine.sendCommand("wipeAll", []);
       }
 
       // Make sure the changed clients get updated.
-      Clients.sync();
+      this.clientsEngine.sync();
     } catch (ex) {
-      ErrorHandler.checkServerError(ex);
+      this.errorHandler.checkServerError(ex);
       throw ex;
     }
   },
 
   /**
    * Reset local service information like logs, sync times, caches.
    */
-  resetService: function resetService()
-    this._catch(this._notify("reset-service", "", function onNotify() {
+  resetService: function resetService() {
+    this._catch(function reset() {
       this._log.info("Service reset.");
 
       // Pretend we've never synced to the server and drop cached data
       this.syncID = "";
-      Records.clearCache();
-    }))(),
+      this.recordManager.clearCache();
+    })();
+  },
 
   /**
    * Reset the client by getting rid of any local server data and client data.
    *
    * @param engines [optional]
    *        Array of engine names to reset. If not given, all engines are used.
    */
-  resetClient: function resetClient(engines)
-    this._catch(this._notify("reset-client", "", function onNotify() {
+  resetClient: function resetClient(engines) {
+    this._catch(function doResetClient() {
       // If we don't have any engines, reset everything including the service
       if (!engines) {
         // Clear out any service data
         this.resetService();
 
-        engines = [Clients].concat(Engines.getAll());
+        engines = [this.clientsEngine].concat(this.engineManager.getAll());
       }
       // Convert the array of names into engines
-      else
-        engines = Engines.get(engines);
+      else {
+        engines = this.engineManager.get(engines);
+      }
 
       // Have each engine drop any temporary meta data
-      for each (let engine in engines)
+      for each (let engine in engines) {
         engine.resetClient();
-    }))(),
+      }
+    })();
+  },
 
   /**
    * Fetch storage info from the server.
    *
    * @param type
    *        String specifying what info to fetch from the server. Must be one
    *        of the INFO_* values. See Sync Storage Server API spec for details.
    * @param callback
@@ -1656,17 +1442,17 @@ WeaveSvc.prototype = {
   getStorageInfo: function getStorageInfo(type, callback) {
     if (STORAGE_INFO_TYPES.indexOf(type) == -1) {
       throw "Invalid value for 'type': " + type;
     }
 
     let info_type = "info/" + type;
     this._log.trace("Retrieving '" + info_type + "'...");
     let url = this.userBaseURL + info_type;
-    return new SyncStorageRequest(url).get(function onComplete(error) {
+    return this.getStorageRequest(url).get(function onComplete(error) {
       // Note: 'this' is the request.
       if (error) {
         this._log.debug("Failed to retrieve '" + info_type + "': " +
                         Utils.exceptionStr(error));
         return callback(error);
       }
       if (this.response.status != 200) {
         this._log.debug("Failed to retrieve '" + info_type +
@@ -1681,15 +1467,13 @@ WeaveSvc.prototype = {
       } catch (ex) {
         this._log.debug("Server returned invalid JSON for '" + info_type +
                         "': " + this.response.body);
         return callback(ex);
       }
       this._log.trace("Successfully retrieved '" + info_type + "'.");
       return callback(null, result);
     });
-  }
-
+  },
 };
 
-// Load Weave on the first time this file is loaded
-let Service = new WeaveSvc();
+let Service = new Sync11Service();
 Service.onStartup();
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/stages/cluster.js
@@ -0,0 +1,95 @@
+/* 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 = ["ClusterManager"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/util.js");
+
+/**
+ * Contains code for managing the Sync cluster we are in.
+ */
+function ClusterManager(service) {
+  this._log = Log4Moz.repository.getLogger("Sync.Service");
+  this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+
+  this.service = service;
+}
+ClusterManager.prototype = {
+  get identity() {
+    return this.service.identity;
+  },
+
+  /**
+   * Obtain the cluster for the current user.
+   *
+   * Returns the string URL of the cluster or null on error.
+   */
+  _findCluster: function _findCluster() {
+    this._log.debug("Finding cluster for user " + this.identity.username);
+
+    // This should ideally use UserAPI10Client but the legacy hackiness is
+    // strong with this code.
+    let fail;
+    let url = this.service.userAPIURI + this.identity.username + "/node/weave";
+    let res = this.service.resource(url);
+    try {
+      let node = res.get();
+      switch (node.status) {
+        case 400:
+          this.service.status.login = LOGIN_FAILED_LOGIN_REJECTED;
+          fail = "Find cluster denied: " + this.service.errorHandler.errorStr(node);
+          break;
+        case 404:
+          this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)");
+          return this.service.serverURL;
+        case 0:
+        case 200:
+          if (node == "null") {
+            node = null;
+          }
+          this._log.trace("_findCluster successfully returning " + node);
+          return node;
+        default:
+          this.service.errorHandler.checkServerError(node);
+          fail = "Unexpected response code: " + node.status;
+          break;
+      }
+    } catch (e) {
+      this._log.debug("Network error on findCluster");
+      this.service.status.login = LOGIN_FAILED_NETWORK_ERROR;
+      this.service.errorHandler.checkServerError(e);
+      fail = e;
+    }
+    throw fail;
+  },
+
+  /**
+   * Determine the cluster for the current user and update state.
+   */
+  setCluster: function setCluster() {
+    // Make sure we didn't get some unexpected response for the cluster.
+    let cluster = this._findCluster();
+    this._log.debug("Cluster value = " + cluster);
+    if (cluster == null) {
+      return false;
+    }
+
+    // Don't update stuff if we already have the right cluster
+    if (cluster == this.service.clusterURL) {
+      return false;
+    }
+
+    this._log.debug("Setting cluster to " + cluster);
+    this.service.clusterURL = cluster;
+    Svc.Prefs.set("lastClusterUpdate", Date.now().toString());
+
+    return true;
+  },
+};
+Object.freeze(ClusterManager.prototype);
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/stages/enginesync.js
@@ -0,0 +1,274 @@
+/* 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/. */
+
+/**
+ * This file contains code for synchronizing engines.
+ */
+
+const EXPORTED_SYMBOLS = ["EngineSynchronizer"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/util.js");
+
+/**
+ * Perform synchronization of engines.
+ *
+ * This was originally split out of service.js. The API needs lots of love.
+ */
+function EngineSynchronizer(service) {
+  this._log = Log4Moz.repository.getLogger("Sync.Synchronizer");
+  this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.synchronizer")];
+
+  this.service = service;
+
+  this.onComplete = null;
+}
+
+EngineSynchronizer.prototype = {
+  sync: function sync() {
+    if (!this.onComplete) {
+      throw new Error("onComplete handler not installed.");
+    }
+
+    let startTime = Date.now();
+
+    this.service.status.resetSync();
+
+    // Make sure we should sync or record why we shouldn't.
+    let reason = this.service._checkSync();
+    if (reason) {
+      if (reason == kSyncNetworkOffline) {
+        this.service.status.sync = LOGIN_FAILED_NETWORK_ERROR;
+      }
+
+      // this is a purposeful abort rather than a failure, so don't set
+      // any status bits
+      reason = "Can't sync: " + reason;
+      this.onComplete(new Error("Can't sync: " + reason));
+      return;
+    }
+
+    // If we don't have a node, get one. If that fails, retry in 10 minutes.
+    if (!this.service.clusterURL && !this.service._clusterManager.setCluster()) {
+      this.service.status.sync = NO_SYNC_NODE_FOUND;
+      this._log.info("No cluster URL found. Cannot sync.");
+      this.onComplete(null);
+      return;
+    }
+
+    // Ping the server with a special info request once a day.
+    let infoURL = this.service.infoURL;
+    let now = Math.floor(Date.now() / 1000);
+    let lastPing = Svc.Prefs.get("lastPing", 0);
+    if (now - lastPing > 86400) { // 60 * 60 * 24
+      infoURL += "?v=" + WEAVE_VERSION;
+      Svc.Prefs.set("lastPing", now);
+    }
+
+    // Figure out what the last modified time is for each collection
+    let info = this.service._fetchInfo(infoURL);
+
+    // Convert the response to an object and read out the modified times
+    for (let engine of [this.service.clientsEngine].concat(this.service.engineManager.getAll())) {
+      engine.lastModified = info.obj[engine.name] || 0;
+    }
+
+    if (!(this.service._remoteSetup(info))) {
+      this.onComplete(new Error("Aborting sync, remote setup failed"));
+      return;
+    }
+
+    // Make sure we have an up-to-date list of clients before sending commands
+    this._log.debug("Refreshing client list.");
+    if (!this._syncEngine(this.service.clientsEngine)) {
+      // Clients is an engine like any other; it can fail with a 401,
+      // and we can elect to abort the sync.
+      this._log.warn("Client engine sync failed. Aborting.");
+      this.onComplete(null);
+      return;
+    }
+
+    // Wipe data in the desired direction if necessary
+    switch (Svc.Prefs.get("firstSync")) {
+      case "resetClient":
+        this.service.resetClient(this.service.enabledEngineNames);
+        break;
+      case "wipeClient":
+        this.service.wipeClient(this.service.enabledEngineNames);
+        break;
+      case "wipeRemote":
+        this.service.wipeRemote(this.service.enabledEngineNames);
+        break;
+    }
+
+    if (this.service.clientsEngine.localCommands) {
+      try {
+        if (!(this.service.clientsEngine.processIncomingCommands())) {
+          this.service.status.sync = ABORT_SYNC_COMMAND;
+          this.onComplete(new Error("Processed command aborted sync."));
+          return;
+        }
+
+        // Repeat remoteSetup in-case the commands forced us to reset
+        if (!(this.service._remoteSetup(info))) {
+          this.onComplete(new Error("Remote setup failed after processing commands."));
+          return;
+        }
+      }
+      finally {
+        // Always immediately attempt to push back the local client (now
+        // without commands).
+        // Note that we don't abort here; if there's a 401 because we've
+        // been reassigned, we'll handle it around another engine.
+        this._syncEngine(this.service.clientsEngine);
+      }
+    }
+
+    // Update engines because it might change what we sync.
+    try {
+      this._updateEnabledEngines();
+    } catch (ex) {
+      this._log.debug("Updating enabled engines failed: " +
+                      Utils.exceptionStr(ex));
+      this.service.errorHandler.checkServerError(ex);
+      this.onComplete(ex);
+      return;
+    }
+
+    try {
+      for each (let engine in this.service.engineManager.getEnabled()) {
+        // If there's any problems with syncing the engine, report the failure
+        if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) {
+          this._log.info("Aborting sync for failure in " + engine.name);
+          break;
+        }
+      }
+
+      // If _syncEngine fails for a 401, we might not have a cluster URL here.
+      // If that's the case, break out of this immediately, rather than
+      // throwing an exception when trying to fetch metaURL.
+      if (!this.service.clusterURL) {
+        this._log.debug("Aborting sync, no cluster URL: " +
+                        "not uploading new meta/global.");
+        this.onComplete(null);
+        return;
+      }
+
+      // Upload meta/global if any engines changed anything
+      let meta = this.service.recordManager.get(this.service.metaURL);
+      if (meta.isNew || meta.changed) {
+        this.service.resource(this.service.metaURL).put(meta);
+        delete meta.isNew;
+        delete meta.changed;
+      }
+
+      // If there were no sync engine failures
+      if (this.service.status.service != SYNC_FAILED_PARTIAL) {
+        Svc.Prefs.set("lastSync", new Date().toString());
+        this.service.status.sync = SYNC_SUCCEEDED;
+      }
+    } finally {
+      Svc.Prefs.reset("firstSync");
+
+      let syncTime = ((Date.now() - startTime) / 1000).toFixed(2);
+      let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
+      this._log.info("Sync completed at " + dateStr
+                     + " after " + syncTime + " secs.");
+    }
+
+    this.onComplete(null);
+  },
+
+  // Returns true if sync should proceed.
+  // false / no return value means sync should be aborted.
+  _syncEngine: function _syncEngine(engine) {
+    try {
+      engine.sync();
+    }
+    catch(e) {
+      if (e.status == 401) {
+        // Maybe a 401, cluster update perhaps needed?
+        // We rely on ErrorHandler observing the sync failure notification to
+        // schedule another sync and clear node assignment values.
+        // Here we simply want to muffle the exception and return an
+        // appropriate value.
+        return false;
+      }
+    }
+
+    return true;
+  },
+
+  _updateEnabledEngines: function _updateEnabledEngines() {
+    this._log.info("Updating enabled engines: " +
+                   this.service.scheduler.numClients + " clients.");
+    let meta = this.service.recordManager.get(this.service.metaURL);
+    if (meta.isNew || !meta.payload.engines)
+      return;
+
+    // If we're the only client, and no engines are marked as enabled,
+    // thumb our noses at the server data: it can't be right.
+    // Belt-and-suspenders approach to Bug 615926.
+    if ((this.service.scheduler.numClients <= 1) &&
+        ([e for (e in meta.payload.engines) if (e != "clients")].length == 0)) {
+      this._log.info("One client and no enabled engines: not touching local engine status.");
+      return;
+    }
+
+    this.service._ignorePrefObserver = true;
+
+    let enabled = this.service.enabledEngineNames;
+    for (let engineName in meta.payload.engines) {
+      if (engineName == "clients") {
+        // Clients is special.
+        continue;
+      }
+      let index = enabled.indexOf(engineName);
+      if (index != -1) {
+        // The engine is enabled locally. Nothing to do.
+        enabled.splice(index, 1);
+        continue;
+      }
+      let engine = this.service.engineManager.get(engineName);
+      if (!engine) {
+        // The engine doesn't exist locally. Nothing to do.
+        continue;
+      }
+
+      if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
+        // The engine was disabled locally. Wipe server data and
+        // disable it everywhere.
+        this._log.trace("Wiping data for " + engineName + " engine.");
+        engine.wipeServer();
+        delete meta.payload.engines[engineName];
+        meta.changed = true;
+      } else {
+        // The engine was enabled remotely. Enable it locally.
+        this._log.trace(engineName + " engine was enabled remotely.");
+        engine.enabled = true;
+      }
+    }
+
+    // Any remaining engines were either enabled locally or disabled remotely.
+    for each (let engineName in enabled) {
+      let engine = this.service.engineManager.get(engineName);
+      if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
+        this._log.trace("The " + engineName + " engine was enabled locally.");
+      } else {
+        this._log.trace("The " + engineName + " engine was disabled remotely.");
+        engine.enabled = false;
+      }
+    }
+
+    Svc.Prefs.resetBranch("engineStatusChanged.");
+    this.service._ignorePrefObserver = false;
+  },
+
+};
+Object.freeze(EngineSynchronizer.prototype);
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -11,17 +11,17 @@ const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.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,
+  _authManager: new IdentityManager(),
   ready: false,
 
   get service() {
     return this._service;
   },
 
   set service(code) {
     this._log.debug("Status.service: " + this._service + " => " + code);
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/userapi.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "UserAPI10Client",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/util.js");
+
+/**
+ * A generic client for the user API 1.0 service.
+ *
+ * http://docs.services.mozilla.com/reg/apis.html
+ *
+ * Instances are constructed with the base URI of the service.
+ */
+function UserAPI10Client(baseURI) {
+  this._log = Log4Moz.repository.getLogger("Sync.UserAPI");
+  this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.userapi")];
+
+  this.baseURI = baseURI;
+}
+UserAPI10Client.prototype = {
+  USER_CREATE_ERROR_CODES: {
+    2: "Incorrect or missing captcha.",
+    4: "User exists.",
+    6: "JSON parse failure.",
+    7: "Missing password field.",
+    9: "Requested password not strong enough.",
+    12: "No email address on file.",
+  },
+
+  /**
+   * Determine whether a specified username exists.
+   *
+   * Callback receives the following arguments:
+   *
+   *   (Error) Describes error that occurred or null if request was
+   *           successful.
+   *   (boolean) True if user exists. False if not. null if there was an error.
+   */
+  usernameExists: function usernameExists(username, cb) {
+    if (typeof(cb) != "function") {
+      throw new Error("cb must be a function.");
+    }
+
+    let url = this.baseURI + username;
+    let request = new RESTRequest(url);
+    request.get(this._onUsername.bind(this, cb, request));
+  },
+
+  /**
+   * Obtain the Weave (Sync) node for a specified user.
+   *
+   * The callback receives the following arguments:
+   *
+   *   (Error)  Describes error that occurred or null if request was successful.
+   *   (string) Username request is for.
+   *   (string) URL of user's node. If null and there is no error, no node could
+   *            be assigned at the time of the request.
+   */
+  getWeaveNode: function getWeaveNode(username, password, cb) {
+    if (typeof(cb) != "function") {
+      throw new Error("cb must be a function.");
+    }
+
+    let request = this._getRequest(username, "/node/weave", password);
+    request.get(this._onWeaveNode.bind(this, cb, request));
+  },
+
+  /**
+   * Change a password for the specified user.
+   *
+   * @param username
+   *        (string) The username whose password to change.
+   * @param oldPassword
+   *        (string) The old, current password.
+   * @param newPassword
+   *        (string) The new password to switch to.
+   */
+  changePassword: function changePassword(username, oldPassword, newPassword, cb) {
+    let request = this._getRequest(username, "/password", oldPassword);
+    request.onComplete = this._onChangePassword.bind(this, cb, request);
+    request.post(CommonUtils.encodeUTF8(newPassword));
+  },
+
+  createAccount: function createAccount(email, password, captchaChallenge,
+                                        captchaResponse, cb) {
+    let username = IdentityManager.prototype.usernameFromAccount(email);
+    let body = JSON.stringify({
+      "email":             email,
+      "password":          Utils.encodeUTF8(password),
+      "captcha-challenge": captchaChallenge,
+      "captcha-response":  captchaResponse
+    });
+
+    let url = this.baseURI + username;
+    let request = new RESTRequest(url);
+
+    if (this.adminSecret) {
+      request.setHeader("X-Weave-Secret", this.adminSecret);
+    }
+
+    request.onComplete = this._onCreateAccount.bind(this, cb, request);
+    request.put(body);
+  },
+
+  _getRequest: function _getRequest(username, path, password=null) {
+    let url = this.baseURI + username + path;
+    let request = new RESTRequest(url);
+
+    if (password) {
+      let up = username + ":" + password;
+      request.setHeader("authorization", "Basic " + btoa(up));
+    }
+
+    return request;
+  },
+
+  _onUsername: function _onUsername(cb, request, error) {
+    if (error) {
+      cb(error, null);
+      return;
+    }
+
+    let body = request.response.body;
+    if (body == "0") {
+      cb(null, false);
+      return;
+    } else if (body == "1") {
+      cb(null, true);
+      return;
+    } else {
+      cb(new Error("Unknown response from server: " + body), null);
+      return;
+    }
+  },
+
+  _onWeaveNode: function _onWeaveNode(cb, request, error) {
+    if (error) {
+      cb.network = true;
+      cb(error, null);
+      return;
+    }
+
+    let response = request.response;
+
+    if (response.status == 200) {
+      let body = response.body;
+      if (body == "null") {
+        cb(null, null);
+        return;
+      }
+
+      cb(null, body);
+      return;
+    }
+
+    let error = new Error("Sync node retrieval failed.");
+    switch (response.status) {
+      case 400:
+        error.denied = true;
+        break;
+      case 404:
+        error.notFound = true;
+        break;
+      default:
+        error.message = "Unexpected response code: " + response.status;
+    }
+
+    cb(error, null);
+    return;
+  },
+
+  _onChangePassword: function _onChangePassword(cb, request, error) {
+    this._log.info("Password change response received: " +
+                   request.response.status);
+    if (error) {
+      cb(error);
+      return;
+    }
+
+    let response = request.response;
+    if (response.status != 200) {
+      cb(new Error("Password changed failed: " + response.body));
+      return;
+    }
+
+    cb(null);
+  },
+
+  _onCreateAccount: function _onCreateAccount(cb, request, error) {
+    let response = request.response;
+
+    this._log.info("Create account response: " + response.status + " " +
+                   response.body);
+
+    if (error) {
+      cb(new Error("HTTP transport error."), null);
+      return;
+    }
+
+    if (response.status == 200) {
+      cb(null, response.body);
+      return;
+    }
+
+    let error = new Error("Could not create user.");
+    error.body = response.body;
+
+    cb(error, null);
+    return;
+  },
+};
+Object.freeze(UserAPI10Client.prototype);
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -64,9 +64,10 @@ pref("services.sync.log.logger.engine.bo
 pref("services.sync.log.logger.engine.clients", "Debug");
 pref("services.sync.log.logger.engine.forms", "Debug");
 pref("services.sync.log.logger.engine.history", "Debug");
 pref("services.sync.log.logger.engine.passwords", "Debug");
 pref("services.sync.log.logger.engine.prefs", "Debug");
 pref("services.sync.log.logger.engine.tabs", "Debug");
 pref("services.sync.log.logger.engine.addons", "Debug");
 pref("services.sync.log.logger.engine.apps", "Debug");
+pref("services.sync.log.logger.userapi", "Debug");
 pref("services.sync.log.cryptoDebug", false);
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,54 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-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 = {
   getFile: function(prop, persistent) {
     persistent.value = true;
     switch (prop) {
       case "ExtPrefDL":
         return [Services.dirsvc.get("CurProcD", Ci.nsIFile)];
       default:
         throw Cr.NS_ERROR_FAILURE;
     }
   },
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider])
 };
 Services.dirsvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider);
 
-let timer;
-function waitForZeroTimer(callback) {
-  // First wait >100ms (nsITimers can take up to that much time to fire, so
-  // we can account for the timer in delayedAutoconnect) and then two event
-  // loop ticks (to account for the Utils.nextTick() in autoConnect).
-  let ticks = 2;
-  function wait() {
-    if (ticks) {
-      ticks -= 1;
-      Utils.nextTick(wait);
-      return;
-    }
-    callback();
-  }
-  timer = Utils.namedTimer(wait, 150, {}, "timer");
-}
-
-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);
   }
 
@@ -140,243 +113,14 @@ function uninstallAddon(addon) {
     }
   }};
 
   AddonManager.addAddonListener(listener);
   addon.uninstall();
   Async.waitForSyncCallback(cb);
 }
 
-function FakeFilesystemService(contents) {
-  this.fakeContents = contents;
-  let self = this;
-
-  Utils.jsonSave = function jsonSave(filePath, that, obj, callback) {
-    let json = typeof obj == "function" ? obj.call(that) : obj;
-    self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
-    callback.call(that);
-  };
-
-  Utils.jsonLoad = function jsonLoad(filePath, that, callback) {
-    let obj;
-    let json = self.fakeContents["weave/" + filePath + ".json"];
-    if (json) {
-      obj = JSON.parse(json);
-    }
-    callback.call(that, obj);
-  };
-};
-
-function FakeGUIDService() {
-  let latestGUID = 0;
-
-  Utils.makeGUID = function fake_makeGUID() {
-    return "fake-guid-" + latestGUID++;
-  };
-}
-
-
-function fakeSHA256HMAC(message) {
-   message = message.substr(0, 64);
-   while (message.length < 64) {
-     message += " ";
-   }
-   return message;
-}
-
-/*
- * Mock implementation of WeaveCrypto. It does not encrypt or
- * decrypt, merely returning the input verbatim.
- */
-function FakeCryptoService() {
-  this.counter = 0;
-
-  delete Svc.Crypto;  // get rid of the getter first
-  Svc.Crypto = this;
-
-  CryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC(keyBundle) {
-    return fakeSHA256HMAC(this.ciphertext);
-  };
-}
-FakeCryptoService.prototype = {
-
-  encrypt: function(aClearText, aSymmetricKey, aIV) {
-    return aClearText;
-  },
-
-  decrypt: function(aCipherText, aSymmetricKey, aIV) {
-    return aCipherText;
-  },
-
-  generateRandomKey: function() {
-    return btoa("fake-symmetric-key-" + this.counter++);
-  },
-
-  generateRandomIV: function() {
-    // A base64-encoded IV is 24 characters long
-    return btoa("fake-fake-fake-random-iv");
-  },
-
-  expandData : function expandData(data, len) {
-    return data;
-  },
-
-  deriveKeyFromPassphrase : function (passphrase, salt, keyLength) {
-    return "some derived key string composed of bytes";
-  },
-
-  generateRandomBytes: function(aByteCount) {
-    return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(aByteCount);
-  }
-};
-
-function setBasicCredentials(username, password, syncKey) {
-  let auth = Identity;
-  auth.username = username;
-  auth.basicPassword = password;
-  auth.syncKey = syncKey;
-}
-
-function SyncTestingInfrastructure(username, password, syncKey) {
-  Cu.import("resource://services-sync/service.js");
-
-  Identity.account = username || "foo";
-  Identity.basicPassword = password || "password";
-  Identity.syncKey = syncKey || "foo";
-
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
-
-  this.logStats = initTestLogging();
-  this.fakeFilesystem = new FakeFilesystemService({});
-  this.fakeGUIDService = new FakeGUIDService();
-  this.fakeCryptoService = new FakeCryptoService();
+function generateNewKeys(collectionKeys, collections=null) {
+  let wbo = collectionKeys.generateNewKeysWBO(collections);
+  let modified = new_timestamp();
+  collectionKeys.setContents(wbo.cleartext, modified);
 }
 
-_("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);
-  }
-
-  return {ciphertext: cleartext, // ciphertext == cleartext with fake crypto
-          IV: "irrelevant",
-          hmac: fakeSHA256HMAC(cleartext, Utils.makeHMACKey(""))};
-}
-
-function generateNewKeys(collections) {
-  let wbo = CollectionKeys.generateNewKeysWBO(collections);
-  let modified = new_timestamp();
-  CollectionKeys.setContents(wbo.cleartext, modified);
-}
-
-/*
- * A fake engine implementation.
- * This is used all over the place.
- *
- * Complete with record, store, and tracker implementations.
- */
-
-function RotaryRecord(collection, id) {
-  CryptoWrapper.call(this, collection, id);
-}
-RotaryRecord.prototype = {
-  __proto__: CryptoWrapper.prototype
-};
-Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]);
-
-function RotaryStore() {
-  Store.call(this, "Rotary");
-  this.items = {};
-}
-RotaryStore.prototype = {
-  __proto__: Store.prototype,
-
-  create: function Store_create(record) {
-    this.items[record.id] = record.denomination;
-  },
-
-  remove: function Store_remove(record) {
-    delete this.items[record.id];
-  },
-
-  update: function Store_update(record) {
-    this.items[record.id] = record.denomination;
-  },
-
-  itemExists: function Store_itemExists(id) {
-    return (id in this.items);
-  },
-
-  createRecord: function(id, collection) {
-    let record = new RotaryRecord(collection, id);
-
-    if (!(id in this.items)) {
-      record.deleted = true;
-      return record;
-    }
-
-    record.denomination = this.items[id] || "Data for new record: " + id;
-    return record;
-  },
-
-  changeItemID: function(oldID, newID) {
-    if (oldID in this.items) {
-      this.items[newID] = this.items[oldID];
-    }
-
-    delete this.items[oldID];
-  },
-
-  getAllIDs: function() {
-    let ids = {};
-    for (let id in this.items) {
-      ids[id] = true;
-    }
-    return ids;
-  },
-
-  wipe: function() {
-    this.items = {};
-  }
-};
-
-function RotaryTracker() {
-  Tracker.call(this, "Rotary");
-}
-RotaryTracker.prototype = {
-  __proto__: Tracker.prototype
-};
-
-
-function RotaryEngine() {
-  SyncEngine.call(this, "Rotary");
-  // Ensure that the engine starts with a clean slate.
-  this.toFetch        = [];
-  this.previousFailed = [];
-}
-RotaryEngine.prototype = {
-  __proto__: SyncEngine.prototype,
-  _storeObj: RotaryStore,
-  _trackerObj: RotaryTracker,
-  _recordObj: RotaryRecord,
-
-  _findDupe: function(item) {
-    // This is a semaphore used for testing proper reconciling on dupe
-    // detection.
-    if (item.id == "DUPE_INCOMING") {
-      return "DUPE_LOCAL";
-    }
-
-    for (let [id, value] in Iterator(this._store.items)) {
-      if (item.denomination == value) {
-        return id;
-      }
-    }
-  }
-};
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,13 +1,10 @@
 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-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.
@@ -432,17 +429,17 @@ function sync_httpd_setup(handlers) {
       = (new ServerWBO("global", {})).handler();
   return httpd_setup(handlers);
 }
 
 /*
  * Track collection modified times. Return closures.
  */
 function track_collections_helper() {
-  
+
   /*
    * Our tracking object.
    */
   let collections = {};
 
   /*
    * Update the timestamp of a collection.
    */
@@ -483,17 +480,17 @@ function track_collections_helper() {
 
     response.setHeader("Content-Type", "application/json");
     response.setHeader("X-Weave-Timestamp",
                        "" + new_timestamp(),
                        false);
     response.setStatusLine(request.httpVersion, 200, "OK");
     response.bodyOutputStream.write(body, body.length);
   }
-  
+
   return {"collections": collections,
           "handler": info_collections,
           "with_updated_collection": with_updated_collection,
           "update_collection": update_collection};
 }
 
 //===========================================================================//
 // httpd.js-based Sync server.                                               //
@@ -775,17 +772,17 @@ SyncServer.prototype = {
   },
 
   /**
    * This is invoked by the HttpServer. `this` is bound to the SyncServer;
    * `handler` is the HttpServer's handler.
    *
    * TODO: need to use the correct Sync API response codes and errors here.
    * TODO: Basic Auth.
-   * TODO: check username in path against username in BasicAuth. 
+   * TODO: check username in path against username in BasicAuth.
    */
   handleDefault: function handleDefault(handler, req, resp) {
     try {
       this._handleDefault(handler, req, resp);
     } catch (e) {
       if (e instanceof HttpError) {
         this.respond(req, resp, e.code, e.description, "", {});
       } else {
@@ -909,17 +906,17 @@ SyncServer.prototype = {
             return;
           }
           coll.collectionHandler(req, resp);
 
           // Spot if this is a DELETE for some IDs, and don't blow away the
           // whole collection!
           //
           // We already handled deleting the WBOs by invoking the deleted
-          // collection's handler. However, in the case of 
+          // collection's handler. However, in the case of
           //
           //   DELETE storage/foobar
           //
           // we also need to remove foobar from the collections map. This
           // clause tries to differentiate the above request from
           //
           //  DELETE storage/foobar?ids=foo,baz
           //
--- a/services/sync/tests/unit/test_addon_utils.js
+++ b/services/sync/tests/unit/test_addon_utils.js
@@ -1,15 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/addonutils.js");
-Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-sync/util.js");
 
 const HTTP_PORT = 8888;
 const SERVER_ADDRESS = "http://127.0.0.1:8888";
 
 let prefs = new Preferences();
 
 prefs.set("extensions.getAddons.get.url",
           SERVER_ADDRESS + "/search/guid:%IDS%");
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -1,30 +1,34 @@
 /* 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-common/async.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/addonsreconciler.js");
-Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 let prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
 
 loadAddonTestFunctions();
 startupManager();
 
-Engines.register(AddonsEngine);
-let engine = Engines.get("addons");
+let engineManager = Service.engineManager;
+
+engineManager.register(AddonsEngine);
+let engine = engineManager.get("addons");
 let reconciler = engine._reconciler;
 let tracker = engine._tracker;
 
 function advance_test() {
   reconciler._addons = {};
   reconciler._changes = [];
 
   let cb = Async.makeSpinningCallback();
@@ -151,17 +155,17 @@ add_test(function test_disabled_install_
 
   const USER       = "foo";
   const PASSWORD   = "password";
   const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
   const ADDON_ID   = "addon1@tests.mozilla.org";
 
   new SyncTestingInfrastructure(USER, PASSWORD, PASSPHRASE);
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
     meta: {global: {engines: {addons: {version: engine.version,
                                       syncID:  engine.syncID}}}},
     crypto: {},
     addons: {}
   };
 
--- a/services/sync/tests/unit/test_addons_reconciler.js
+++ b/services/sync/tests/unit/test_addons_reconciler.js
@@ -1,28 +1,30 @@
 /* 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://services-sync/addonsreconciler.js");
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
 
 loadAddonTestFunctions();
 startupManager();
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.AddonsReconciler").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.AddonsReconciler").level =
     Log4Moz.Level.Trace;
 
   Svc.Prefs.set("engine.addons", true);
-  Engines.register(AddonsEngine);
+  Service.engineManager.register(AddonsEngine);
 
   run_next_test();
 }
 
 add_test(function test_defaults() {
   _("Ensure new objects have reasonable defaults.");
 
   let reconciler = new AddonsReconciler();
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -1,28 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/addonutils.js");
 Cu.import("resource://services-sync/engines/addons.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.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();
 startupManager();
 
-Engines.register(AddonsEngine);
-let engine     = Engines.get("addons");
+Service.engineManager.register(AddonsEngine);
+let engine     = Service.engineManager.get("addons");
 let tracker    = engine._tracker;
 let store      = engine._store;
 let reconciler = engine._reconciler;
 
 /**
  * Create a AddonsRec for this application with the fields specified.
  *
  * @param  id       Sync GUID of record
--- a/services/sync/tests/unit/test_addons_tracker.js
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -1,25 +1,26 @@
 /* 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://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://gre/modules/AddonManager.jsm");
 
 loadAddonTestFunctions();
 startupManager();
 Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 Svc.Prefs.set("engine.addons", true);
 
-Engines.register(AddonsEngine);
-let engine     = Engines.get("addons");
+Service.engineManager.register(AddonsEngine);
+let engine     = Service.engineManager.get("addons");
 let reconciler = engine._reconciler;
 let store      = engine._store;
 let tracker    = engine._tracker;
 
 const addon1ID = "addon1@tests.mozilla.org";
 
 function cleanup_and_advance() {
   Svc.Obs.notify("weave:engine:stop-tracking");
--- a/services/sync/tests/unit/test_bookmark_batch_fail.js
+++ b/services/sync/tests/unit/test_bookmark_batch_fail.js
@@ -1,13 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Making sure a failing sync reports a useful error");
 Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
 
 function run_test() {
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   engine._syncStartup = function() {
     throw "FAIL!";
   };
 
   try {
     _("Try calling the sync that should throw right away");
     engine._sync();
     do_throw("Should have failed sync!");
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -1,26 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/log4moz.js");
 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-common/log4moz.js");
-Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-
-Engines.register(BookmarksEngine);
+Service.engineManager.register(BookmarksEngine);
 var syncTesting = new SyncTestingInfrastructure();
 
 add_test(function bad_record_allIDs() {
   let syncTesting = new SyncTestingInfrastructure();
 
   _("Ensure that bad Places queries don't cause an error in getAllIDs.");
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store = engine._store;
   let badRecordID = PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.toolbarFolder,
       Utils.makeURI("place:folder=1138"),
       PlacesUtils.bookmarks.DEFAULT_INDEX,
       null);
 
   do_check_true(badRecordID > 0);
@@ -41,17 +43,17 @@ add_test(function bad_record_allIDs() {
   PlacesUtils.bookmarks.removeItem(badRecordID);
   run_next_test();
 });
 
 add_test(function test_ID_caching() {
   let syncTesting = new SyncTestingInfrastructure();
 
   _("Ensure that Places IDs are not cached.");
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store = engine._store;
   _("All IDs: " + JSON.stringify(store.getAllIDs()));
 
   let mobileID = store.idForGUID("mobile");
   _("Change the GUID for that item, and drop the mobile anno.");
   store._setGUID(mobileID, "abcdefghijkl");
   PlacesUtils.annotations.removeItemAnnotation(mobileID, "mobile/bookmarksRoot");
 
@@ -89,17 +91,17 @@ function serverForFoo(engine) {
     bookmarks: {}
   });
 }
 
 add_test(function test_processIncoming_error_orderChildren() {
   _("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
   new SyncTestingInfrastructure();
 
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   let collection = server.user("foo").collection("bookmarks");
 
   try {
 
     let folder1_id = PlacesUtils.bookmarks.createFolder(
@@ -150,26 +152,26 @@ add_test(function test_processIncoming_e
     do_check_eq(new_children[1], folder1_payload.children[1]);
 
     do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk1_id), 1);
     do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk2_id), 0);
 
   } finally {
     store.wipe();
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_restorePromptsReupload() {
   _("Ensure that restoring from a backup will reupload all records.");
   new SyncTestingInfrastructure();
 
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   let collection = server.user("foo").collection("bookmarks");
 
   Svc.Obs.notify("weave:engine:start-tracking");   // We skip usual startup...
 
   try {
@@ -281,17 +283,17 @@ add_test(function test_restorePromptsReu
 
     _("Our old friend Folder 1 is still in play.");
     do_check_eq(folderWBOs.length, 1);
     do_check_eq(folderWBOs[0].title, "Folder 1");
 
   } finally {
     store.wipe();
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 // Bug 632287.
 add_test(function test_mismatched_types() {
   _("Ensure that handling a record that changes type causes deletion " +
     "then re-adding.");
@@ -324,17 +326,17 @@ add_test(function test_mismatched_types(
       ["HCRq40Rnxhrd", "YeyWCV1RVsYw", "GCceVZMhvMbP", "sYi2hevdArlF",
        "vjbZlPlSyGY8", "UtjUhVyrpeG6", "rVq8WMG2wfZI", "Lx0tcy43ZKhZ",
        "oT74WwV8_j4P", "IztsItWVSo3-"],
     "parentid": "toolbar"
   };
 
   new SyncTestingInfrastructure();
 
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   _("GUID: " + store.GUIDForId(6, true));
 
   try {
     let bms = PlacesUtils.bookmarks;
     let oldR = new FakeRecord(BookmarkFolder, oldRecord);
@@ -357,27 +359,27 @@ add_test(function test_mismatched_types(
     _("Applied new. It's a livemark.");
     do_check_eq(bms.getItemType(newID), bms.TYPE_FOLDER);
     do_check_true(PlacesUtils.annotations
                              .itemHasAnnotation(newID, PlacesUtils.LMANNO_FEEDURI));
 
   } finally {
     store.wipe();
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_bookmark_guidMap_fail() {
   _("Ensure that failures building the GUID map cause early death.");
 
   new SyncTestingInfrastructure();
 
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store = engine._store;
 
   let store  = engine._store;
   let server = serverForFoo(engine);
   let coll   = server.user("foo").collection("bookmarks");
 
   // Add one item to the server.
   let itemID = PlacesUtils.bookmarks.createFolder(
@@ -414,17 +416,17 @@ add_test(function test_bookmark_guidMap_
     err = ex;
   }
   do_check_eq(err, "Nooo");
 
   server.stop(run_next_test);
 });
 
 add_test(function test_bookmark_is_taggable() {
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store = engine._store;
 
   do_check_true(store.isTaggable("bookmark"));
   do_check_true(store.isTaggable("microsummary"));
   do_check_true(store.isTaggable("query"));
   do_check_false(store.isTaggable("folder"));
   do_check_false(store.isTaggable("livemark"));
   do_check_false(store.isTaggable(null));
@@ -432,22 +434,22 @@ add_test(function test_bookmark_is_tagga
   do_check_false(store.isTaggable(""));
 
   run_next_test();
 });
 
 add_test(function test_bookmark_tag_but_no_uri() {
   _("Ensure that a bookmark record with tags, but no URI, doesn't throw an exception.");
 
-  let engine = new BookmarksEngine();
+  let engine = new BookmarksEngine(Service);
   let store = engine._store;
 
   // We're simply checking that no exception is thrown, so
   // no actual checks in this test.
- 
+
   store._tagURI(null, ["foo"]);
   store._tagURI(null, null);
   store._tagURI(Utils.makeURI("about:fake"), null);
 
   let record = {
     _parent:     PlacesUtils.bookmarks.toolbarFolder,
     id:          Utils.makeGUID(),
     description: "",
@@ -467,12 +469,12 @@ add_test(function test_bookmark_tag_but_
 });
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level  = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level   = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Tracker.Bookmarks").level = Log4Moz.Level.Trace;
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
+++ b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
@@ -1,23 +1,23 @@
 /* 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://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
+Cu.import("resource://services-common/log4moz.js");
 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-common/log4moz.js");
+Cu.import("resource://services-sync/service.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";
 
 const TEST_URL = "http://micsum.mozilla.org/";
 const TEST_TITLE = "A microsummarized bookmark"
 const GENERATOR_URL = "http://generate.micsum/"
 const STATIC_TITLE = "Static title"
 
@@ -32,18 +32,18 @@ function newMicrosummary(url, title) {
   PlacesUtils.annotations.setItemAnnotation(id, STATICTITLE_ANNO,
                                             "Static title", 0,
                                             PlacesUtils.annotations.EXPIRE_NEVER);
   return id;
 }
 
 function run_test() {
 
-  Engines.register(BookmarksEngine);
-  let engine = Engines.get("bookmarks");
+  Service.engineManager.register(BookmarksEngine);
+  let engine = Service.engineManager.get("bookmarks");
   let store = engine._store;
 
   // Clean up.
   store.wipe();
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
 
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -7,17 +7,17 @@ Cu.import("resource://services-sync/engi
 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");
 Cu.import("resource://testing-common/services-common/utils.js");
 
 const DESCRIPTION_ANNO = "bookmarkProperties/description";
 
-let engine = Engines.get("bookmarks");
+let engine = Service.engineManager.get("bookmarks");
 let store = engine._store;
 
 // Record borrowed from Bug 631361.
 let record631361 = {
   id: "M5bwUKK8hPyF",
   index: 150,
   modified: 1296768176.49,
   payload:
@@ -55,17 +55,17 @@ let record631361 = {
 
 // Clean up after other tests. Only necessary in XULRunner.
 store.wipe();
 
 function makeLivemark(p, mintGUID) {
   let b = new Livemark("bookmarks", p.id);
   // Copy here, because tests mutate the contents.
   b.cleartext = TestingUtils.deepCopy(p);
-  
+
   if (mintGUID)
     b.id = Utils.makeGUID();
 
   return b;
 }
 
 
 function run_test() {
@@ -80,64 +80,64 @@ add_test(function test_livemark_descript
   let record = record631361.payload;
 
   function doRecord(r) {
     store._childrenToOrder = {};
     store.applyIncoming(r);
     store._orderChildren();
     delete store._childrenToOrder;
   }
-  
+
   // Attempt to provoke an error by messing around with the description.
   record.description = null;
   doRecord(makeLivemark(record));
   record.description = "";
   doRecord(makeLivemark(record));
-  
+
   // Attempt to provoke an error by adding a bad description anno.
   let id = store.idForGUID(record.id);
   PlacesUtils.annotations.setItemAnnotation(id, DESCRIPTION_ANNO, "", 0,
                                             PlacesUtils.annotations.EXPIRE_NEVER);
 
   run_next_test();
 });
 
 add_test(function test_livemark_invalid() {
   _("Livemarks considered invalid by nsLivemarkService are skipped.");
-  
+
   _("Parent is 0, which is invalid. Will be set to unfiled.");
   let noParentRec = makeLivemark(record631361.payload, true);
   noParentRec._parent = 0;
   store.create(noParentRec);
   let recID = store.idForGUID(noParentRec.id, true);
   do_check_true(recID > 0);
   do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(recID), PlacesUtils.bookmarks.unfiledBookmarksFolder);
-  
+
   _("Parent is unknown. Will be set to unfiled.");
   let lateParentRec = makeLivemark(record631361.payload, true);
   let parentGUID = Utils.makeGUID();
   lateParentRec.parentid = parentGUID;
   lateParentRec._parent = store.idForGUID(parentGUID);   // Usually done by applyIncoming.
   do_check_eq(-1, lateParentRec._parent);
-  
+
   store.create(lateParentRec);
   recID = store.idForGUID(lateParentRec.id, true);
   do_check_true(recID > 0);
   do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(recID),
               PlacesUtils.bookmarks.unfiledBookmarksFolder);
-  
+
   _("No feed URI, which is invalid. Will be skipped.");
   let noFeedURIRec = makeLivemark(record631361.payload, true);
   delete noFeedURIRec.cleartext.feedUri;
   store.create(noFeedURIRec);
   // No exception, but no creation occurs.
   do_check_eq(-1, store.idForGUID(noFeedURIRec.id, true));
 
   _("Parent is a Livemark. Will be skipped.");
   let lmParentRec = makeLivemark(record631361.payload, true);
   lmParentRec._parent = recID;
   store.create(lmParentRec);
   // No exception, but no creation occurs.
   do_check_eq(-1, store.idForGUID(lmParentRec.id, true));
-  
+
   // Clear event loop.
   Utils.nextTick(run_next_test);
 });
--- a/services/sync/tests/unit/test_bookmark_order.js
+++ b/services/sync/tests/unit/test_bookmark_order.js
@@ -1,10 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Making sure after processing incoming bookmarks, they show up in the right order");
 Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 function getBookmarks(folderId) {
   let bookmarks = [];
 
   let pos = 0;
   while (true) {
     let itemId = PlacesUtils.bookmarks.getIdForItemAt(folderId, pos);
@@ -33,17 +37,17 @@ function check(expected) {
   let bookmarks = getBookmarks(PlacesUtils.bookmarks.unfiledBookmarksFolder);
 
   _("Checking if the bookmark structure is", JSON.stringify(expected));
   _("Got bookmarks:", JSON.stringify(bookmarks));
   do_check_true(Utils.deepEquals(bookmarks, expected));
 }
 
 function run_test() {
-  let store = new BookmarksEngine()._store;
+  let store = new BookmarksEngine(Service)._store;
   initTestLogging("Trace");
 
   _("Starting with a clean slate of no bookmarks");
   store.wipe();
   check([]);
 
   function bookmark(name, parent) {
     let bookmark = new Bookmark("http://weave.server/my-bookmark");
--- a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
+++ b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
@@ -1,13 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Rewrite place: URIs.");
 Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
-let engine = new BookmarksEngine();
+let engine = new BookmarksEngine(Service);
 let store = engine._store;
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level = Log4Moz.Level.Trace;
 
   let tagRecord = new BookmarkQuery("bookmarks", "abcdefabcdef");
@@ -16,31 +20,31 @@ function run_test() {
   tagRecord.parentName = "Bookmarks Toolbar";
   tagRecord.bmkUri = uri;
   tagRecord.title = "tagtag";
   tagRecord.folderName = "bar";
 
   _("Type: " + tagRecord.type);
   _("Folder name: " + tagRecord.folderName);
   store.preprocessTagQuery(tagRecord);
-  
+
   _("Verify that the URI has been rewritten.");
   do_check_neq(tagRecord.bmkUri, uri);
-  
+
   let tags = store._getNode(PlacesUtils.tagsFolderId);
   tags.containerOpen = true;
   let tagID;
   for (let i = 0; i < tags.childCount; ++i) {
     let child = tags.getChild(i);
     if (child.title == "bar")
       tagID = child.itemId;
   }
   tags.containerOpen = false;
 
   _("Tag ID: " + tagID);
   do_check_eq(tagRecord.bmkUri, uri.replace("499", tagID));
-  
+
   _("... but not if the type is wrong.");
   let wrongTypeURI = "place:folder=499&type=2&queryType=1";
   tagRecord.bmkUri = wrongTypeURI;
   store.preprocessTagQuery(tagRecord);
   do_check_eq(tagRecord.bmkUri, wrongTypeURI);
 }
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,46 +1,46 @@
 /* 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-common/log4moz.js");
+Cu.import("resource://services-sync/engines/bookmarks.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-common/log4moz.js");
+Cu.import("resource://services-sync/service.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;
 }
 
 function run_test() {
-  Identity.username = "john@example.com";
-  Identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
-  generateNewKeys();
-  let keyBundle = Identity.syncKeyBundle;
+  Service.identity.username = "john@example.com";
+  Service.identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
+  generateNewKeys(Service.collectionKeys);
+  let keyBundle = Service.identity.syncKeyBundle;
 
   let log = Log4Moz.repository.getLogger("Test");
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   log.info("Creating a record");
 
   let u = "http://localhost:8080/storage/bookmarks/foo";
   let placesItem = new PlacesItem("bookmarks", "foo", "bookmark");
   let bookmarkItem = prepareBookmarkItem("bookmarks", "foo");
-  
+
   log.info("Checking getTypeObject");
   do_check_eq(placesItem.getTypeObject(placesItem.type), Bookmark);
   do_check_eq(bookmarkItem.getTypeObject(bookmarkItem.type), Bookmark);
-  
+
   bookmarkItem.encrypt(keyBundle);
   log.info("Ciphertext is " + bookmarkItem.ciphertext);
   do_check_true(bookmarkItem.ciphertext != null);
-  
+
   log.info("Decrypting the record");
 
   let payload = bookmarkItem.decrypt(keyBundle);
   do_check_eq(payload.stuff, "my payload here");
   do_check_eq(bookmarkItem.getTypeObject(bookmarkItem.type), Bookmark);
   do_check_neq(payload, bookmarkItem.payload); // wrap.data.payload is the encrypted one
 }
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -1,25 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://services-common/log4moz.js");
 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-common/log4moz.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
-
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 var IOService = Cc["@mozilla.org/network/io-service;1"]
                 .getService(Ci.nsIIOService);
 ("http://www.mozilla.com", null, null);
 
 
-Engines.register(BookmarksEngine);
-let engine = Engines.get("bookmarks");
+Service.engineManager.register(BookmarksEngine);
+let engine = Service.engineManager.get("bookmarks");
 let store = engine._store;
 
 // Clean up after other tests. Only necessary in XULRunner.
 store.wipe();
 
 var syncTesting = new SyncTestingInfrastructure();
 
 function newSmartBookmark(parent, uri, position, title, queryID) {
@@ -56,19 +58,19 @@ function serverForFoo(engine) {
 }
 
 // Verify that Places smart bookmarks have their annotation uploaded and
 // handled locally.
 add_test(function test_annotation_uploaded() {
   new SyncTestingInfrastructure();
 
   let startCount = smartBookmarkCount();
-  
+
   _("Start count is " + startCount);
-  
+
   if (startCount > 0) {
     // This can happen in XULRunner.
     clearBookmarks();
     _("Start count is now " + startCount);
   }
 
   _("Create a smart bookmark in the toolbar.");
   let parent = PlacesUtils.toolbarFolderId;
@@ -123,17 +125,17 @@ add_test(function test_annotation_upload
     do_check_true(!!serverWBO);
     let body = JSON.parse(JSON.parse(serverWBO.payload).ciphertext);
     do_check_eq(body.queryId, "MostVisited");
 
     _("We still have the right count.");
     do_check_eq(smartBookmarkCount(), startCount + 1);
 
     _("Clear local records; now we can't find it.");
-    
+
     // "Clear" by changing attributes: if we delete it, apparently it sticks
     // around as a deleted record...
     PlacesUtils.bookmarks.setItemTitle(mostVisitedID, "Not Most Visited");
     PlacesUtils.bookmarks.changeBookmarkURI(
       mostVisitedID, Utils.makeURI("http://something/else"));
     PlacesUtils.annotations.removeItemAnnotation(mostVisitedID,
                                                  SMART_BOOKMARKS_ANNO);
     store.wipe();
@@ -148,88 +150,88 @@ add_test(function test_annotation_upload
     do_check_eq(smartBookmarkCount(), startCount + 1);
 
     _("Find by GUID and verify that it's annotated.");
     let newID = store.idForGUID(serverGUID);
     let newAnnoValue = PlacesUtils.annotations.getItemAnnotation(
       newID, SMART_BOOKMARKS_ANNO);
     do_check_eq(newAnnoValue, "MostVisited");
     do_check_eq(PlacesUtils.bookmarks.getBookmarkURI(newID).spec, uri.spec);
-    
+
     _("Test updating.");
     let newRecord = store.createRecord(serverGUID);
     do_check_eq(newRecord.queryId, newAnnoValue);
     newRecord.queryId = "LeastVisited";
     store.update(newRecord);
     do_check_eq("LeastVisited", PlacesUtils.annotations.getItemAnnotation(
       newID, SMART_BOOKMARKS_ANNO));
-    
+
 
   } finally {
     // Clean up.
     store.wipe();
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_smart_bookmarks_duped() {
   new SyncTestingInfrastructure();
 
   let parent = PlacesUtils.toolbarFolderId;
   let uri =
     Utils.makeURI("place:sort=" +
                   Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING +
                   "&maxResults=10");
   let title = "Most Visited";
   let mostVisitedID = newSmartBookmark(parent, uri, -1, title, "MostVisited");
   let mostVisitedGUID = store.GUIDForId(mostVisitedID);
-  
+
   let record = store.createRecord(mostVisitedGUID);
-  
+
   _("Prepare sync.");
   let server = serverForFoo(engine);
   let collection = server.user("foo").collection("bookmarks");
 
   try {
     engine._syncStartup();
-    
+
     _("Verify that mapDupe uses the anno, discovering a dupe regardless of URI.");
     do_check_eq(mostVisitedGUID, engine._mapDupe(record));
-    
+
     record.bmkUri = "http://foo/";
     do_check_eq(mostVisitedGUID, engine._mapDupe(record));
     do_check_neq(PlacesUtils.bookmarks.getBookmarkURI(mostVisitedID).spec,
                  record.bmkUri);
-    
+
     _("Verify that different annos don't dupe.");
     let other = new BookmarkQuery("bookmarks", "abcdefabcdef");
     other.queryId = "LeastVisited";
     other.parentName = "Bookmarks Toolbar";
     other.bmkUri = "place:foo";
     other.title = "";
     do_check_eq(undefined, engine._findDupe(other));
-    
+
     _("Handle records without a queryId entry.");
     record.bmkUri = uri;
     delete record.queryId;
     do_check_eq(mostVisitedGUID, engine._mapDupe(record));
-    
+
     engine._syncFinish();
 
   } finally {
     // Clean up.
     store.wipe();
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
   }
 });
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -1,16 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 const PARENT_ANNO = "sync/parent";
 
-Engines.register(BookmarksEngine);
-let engine = Engines.get("bookmarks");
+Service.engineManager.register(BookmarksEngine);
+let engine = Service.engineManager.get("bookmarks");
 let store = engine._store;
 let fxuri = Utils.makeURI("http://getfirefox.com/");
 let tburi = Utils.makeURI("http://getthunderbird.com/");
 
 
 function test_bookmark_create() {
   try {
     _("Ensure the record isn't present yet.");
@@ -155,17 +159,17 @@ function test_folder_create() {
     do_check_eq(PlacesUtils.bookmarks.getItemTitle(id), folder.title);
     do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(id),
                 PlacesUtils.bookmarks.toolbarFolder);
 
     _("Have the store create a new record object. Verify that it has the same data.");
     let newrecord = store.createRecord(folder.id);
     do_check_true(newrecord instanceof BookmarkFolder);
     for each (let property in ["title", "parentName", "parentid"])
-      do_check_eq(newrecord[property], folder[property]);      
+      do_check_eq(newrecord[property], folder[property]);
 
     _("Folders have high sort index to ensure they're synced first.");
     do_check_eq(newrecord.sortindex, 1000000);
   } finally {
     _("Clean up.");
     store.wipe();
   }
 }
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -1,16 +1,20 @@
-Cu.import("resource://services-sync/engines/bookmarks.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
-Engines.register(BookmarksEngine);
-let engine = Engines.get("bookmarks");
+Service.engineManager.register(BookmarksEngine);
+let engine = Service.engineManager.get("bookmarks");
 let store  = engine._store;
 store.wipe();
 
 function test_tracking() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
   do_check_empty(tracker.changedIDs);
 
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -1,27 +1,28 @@
 /* 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/record.js");
-Cu.import("resource://services-sync/identity.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/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
 const LESS_THAN_CLIENTS_TTL_REFRESH = 86400;  // 1 day
 
+let engine = Service.clientsEngine;
+
 add_test(function test_bad_hmac() {
   _("Ensure that Clients engine deletes corrupt records.");
   let contents = {
-    meta: {global: {engines: {clients: {version: Clients.version,
-                                        syncID: Clients.syncID}}}},
+    meta: {global: {engines: {clients: {version: engine.version,
+                                        syncID: engine.syncID}}}},
     clients: {},
     crypto: {}
   };
   let deletedCollections = [];
   let deletedItems       = [];
   let callback = {
     __proto__: SyncServerCallback,
     onItemDeleted: function (username, coll, wboID) {
@@ -44,233 +45,233 @@ add_test(function test_bad_hmac() {
 
   function check_client_deleted(id) {
     let coll = user.collection("clients");
     let wbo  = coll.wbo(id);
     return !wbo || !wbo.payload;
   }
 
   function uploadNewKeys() {
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
-    do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+    serverKeys.encrypt(Service.identity.syncKeyBundle);
+    do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
   }
 
   try {
     let passphrase     = "abcdeabcdeabcdeabcdeabcdea";
     Service.serverURL  = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
     Service.login("foo", "ilovejane", passphrase);
 
-    generateNewKeys();
+    generateNewKeys(Service.collectionKeys);
 
     _("First sync, client record is uploaded");
-    do_check_eq(Clients.lastRecordUpload, 0);
+    do_check_eq(engine.lastRecordUpload, 0);
     check_clients_count(0);
-    Clients._sync();
+    engine._sync();
     check_clients_count(1);
-    do_check_true(Clients.lastRecordUpload > 0);
+    do_check_true(engine.lastRecordUpload > 0);
 
     // Initial setup can wipe the server, so clean up.
     deletedCollections = [];
     deletedItems       = [];
 
     _("Change our keys and our client ID, reupload keys.");
-    let oldLocalID  = Clients.localID;     // Preserve to test for deletion!
-    Clients.localID = Utils.makeGUID();
-    Clients.resetClient();
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
-    do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
+    let oldLocalID  = engine.localID;     // Preserve to test for deletion!
+    engine.localID = Utils.makeGUID();
+    engine.resetClient();
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+    serverKeys.encrypt(Service.identity.syncKeyBundle);
+    do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
 
     _("Sync.");
-    Clients._sync();
+    engine._sync();
 
     _("Old record " + oldLocalID + " was deleted, new one uploaded.");
     check_clients_count(1);
     check_client_deleted(oldLocalID);
 
     _("Now change our keys but don't upload them. " +
       "That means we get an HMAC error but redownload keys.");
     Service.lastHMACEvent = 0;
-    Clients.localID = Utils.makeGUID();
-    Clients.resetClient();
-    generateNewKeys();
+    engine.localID = Utils.makeGUID();
+    engine.resetClient();
+    generateNewKeys(Service.collectionKeys);
     deletedCollections = [];
     deletedItems       = [];
     check_clients_count(1);
-    Clients._sync();
+    engine._sync();
 
     _("Old record was not deleted, new one uploaded.");
     do_check_eq(deletedCollections.length, 0);
     do_check_eq(deletedItems.length, 0);
     check_clients_count(2);
 
     _("Now try the scenario where our keys are wrong *and* there's a bad record.");
     // Clean up and start fresh.
     user.collection("clients")._wbos = {};
     Service.lastHMACEvent = 0;
-    Clients.localID = Utils.makeGUID();
-    Clients.resetClient();
+    engine.localID = Utils.makeGUID();
+    engine.resetClient();
     deletedCollections = [];
     deletedItems       = [];
     check_clients_count(0);
 
     uploadNewKeys();
 
     // Sync once to upload a record.
-    Clients._sync();
+    engine._sync();
     check_clients_count(1);
 
     // Generate and upload new keys, so the old client record is wrong.
     uploadNewKeys();
 
     // Create a new client record and new keys. Now our keys are wrong, as well
     // as the object on the server. We'll download the new keys and also delete
     // the bad client record.
-    oldLocalID  = Clients.localID;         // Preserve to test for deletion!
-    Clients.localID = Utils.makeGUID();
-    Clients.resetClient();
-    generateNewKeys();
-    let oldKey = CollectionKeys.keyForCollection();
+    oldLocalID  = engine.localID;         // Preserve to test for deletion!
+    engine.localID = Utils.makeGUID();
+    engine.resetClient();
+    generateNewKeys(Service.collectionKeys);
+    let oldKey = Service.collectionKeys.keyForCollection();
 
     do_check_eq(deletedCollections.length, 0);
     do_check_eq(deletedItems.length, 0);
-    Clients._sync();
+    engine._sync();
     do_check_eq(deletedItems.length, 1);
     check_client_deleted(oldLocalID);
     check_clients_count(1);
-    let newKey = CollectionKeys.keyForCollection();
+    let newKey = Service.collectionKeys.keyForCollection();
     do_check_false(oldKey.equals(newKey));
 
   } finally {
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_properties() {
   _("Test lastRecordUpload property");
   try {
     do_check_eq(Svc.Prefs.get("clients.lastRecordUpload"), undefined);
-    do_check_eq(Clients.lastRecordUpload, 0);
+    do_check_eq(engine.lastRecordUpload, 0);
 
     let now = Date.now();
-    Clients.lastRecordUpload = now / 1000;
-    do_check_eq(Clients.lastRecordUpload, Math.floor(now / 1000));
+    engine.lastRecordUpload = now / 1000;
+    do_check_eq(engine.lastRecordUpload, Math.floor(now / 1000));
   } finally {
     Svc.Prefs.resetBranch("");
     run_next_test();
   }
 });
 
 add_test(function test_sync() {
   _("Ensure that Clients engine uploads a new client record once a week.");
 
   new SyncTestingInfrastructure();
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
-    meta: {global: {engines: {clients: {version: Clients.version,
-                                        syncID: Clients.syncID}}}},
+    meta: {global: {engines: {clients: {version: engine.version,
+                                        syncID: engine.syncID}}}},
     clients: {},
     crypto: {}
   };
   let server = serverForUsers({"foo": "password"}, contents);
   let user   = server.user("foo");
 
   function clientWBO() {
-    return user.collection("clients").wbo(Clients.localID);
+    return user.collection("clients").wbo(engine.localID);
   }
 
   try {
 
     _("First sync. Client record is uploaded.");
     do_check_eq(clientWBO(), undefined);
-    do_check_eq(Clients.lastRecordUpload, 0);
-    Clients._sync();
+    do_check_eq(engine.lastRecordUpload, 0);
+    engine._sync();
     do_check_true(!!clientWBO().payload);
-    do_check_true(Clients.lastRecordUpload > 0);
+    do_check_true(engine.lastRecordUpload > 0);
 
     _("Let's time travel more than a week back, new record should've been uploaded.");
-    Clients.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
-    let lastweek = Clients.lastRecordUpload;
+    engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
+    let lastweek = engine.lastRecordUpload;
     clientWBO().payload = undefined;
-    Clients._sync();
+    engine._sync();
     do_check_true(!!clientWBO().payload);
-    do_check_true(Clients.lastRecordUpload > lastweek);
+    do_check_true(engine.lastRecordUpload > lastweek);
 
     _("Remove client record.");
-    Clients.removeClientData();
+    engine.removeClientData();
     do_check_eq(clientWBO().payload, undefined);
 
     _("Time travel one day back, no record uploaded.");
-    Clients.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
-    let yesterday = Clients.lastRecordUpload;
-    Clients._sync();
+    engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
+    let yesterday = engine.lastRecordUpload;
+    engine._sync();
     do_check_eq(clientWBO().payload, undefined);
-    do_check_eq(Clients.lastRecordUpload, yesterday);
+    do_check_eq(engine.lastRecordUpload, yesterday);
 
   } finally {
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_client_name_change() {
   _("Ensure client name change incurs a client record update.");
 
-  let tracker = Clients._tracker;
+  let tracker = engine._tracker;
 
-  let localID = Clients.localID;
-  let initialName = Clients.localName;
+  let localID = engine.localID;
+  let initialName = engine.localName;
 
   Svc.Obs.notify("weave:engine:start-tracking");
   _("initial name: " + initialName);
 
   // Tracker already has data, so clear it.
   tracker.clearChangedIDs();
 
   let initialScore = tracker.score;
 
   do_check_eq(Object.keys(tracker.changedIDs).length, 0);
 
   Svc.Prefs.set("client.name", "new name");
 
-  _("new name: " + Clients.localName);
-  do_check_neq(initialName, Clients.localName);
+  _("new name: " + engine.localName);
+  do_check_neq(initialName, engine.localName);
   do_check_eq(Object.keys(tracker.changedIDs).length, 1);
-  do_check_true(Clients.localID in tracker.changedIDs);
+  do_check_true(engine.localID in tracker.changedIDs);
   do_check_true(tracker.score > initialScore);
   do_check_true(tracker.score >= SCORE_INCREMENT_XLARGE);
 
   Svc.Obs.notify("weave:engine:stop-tracking");
 
   run_next_test();
 });
 
 add_test(function test_send_command() {
   _("Verifies _sendCommandToClient puts commands in the outbound queue.");
 
-  let store = Clients._store;
-  let tracker = Clients._tracker;
+  let store = engine._store;
+  let tracker = engine._tracker;
   let remoteId = Utils.makeGUID();
   let rec = new ClientsRec("clients", remoteId);
 
   store.create(rec);
   let remoteRecord = store.createRecord(remoteId, "clients");
 
   let action = "testCommand";
   let args = ["foo", "bar"];
 
-  Clients._sendCommandToClient(action, args, remoteId);
+  engine._sendCommandToClient(action, args, remoteId);
 
   let newRecord = store._remoteClients[remoteId];
   do_check_neq(newRecord, undefined);
   do_check_eq(newRecord.commands.length, 1);
 
   let command = newRecord.commands[0];
   do_check_eq(command.command, action);
   do_check_eq(command.args.length, 2);
@@ -279,17 +280,17 @@ add_test(function test_send_command() {
   do_check_neq(tracker.changedIDs[remoteId], undefined);
 
   run_next_test();
 });
 
 add_test(function test_command_validation() {
   _("Verifies that command validation works properly.");
 
-  let store = Clients._store;
+  let store = engine._store;
 
   let testCommands = [
     ["resetAll",    [],       true ],
     ["resetAll",    ["foo"],  false],
     ["resetEngine", ["tabs"], true ],
     ["resetEngine", [],       false],
     ["wipeAll",     [],       true ],
     ["wipeAll",     ["foo"],  false],
@@ -302,212 +303,212 @@ add_test(function test_command_validatio
 
   for each (let [action, args, expectedResult] in testCommands) {
     let remoteId = Utils.makeGUID();
     let rec = new ClientsRec("clients", remoteId);
 
     store.create(rec);
     store.createRecord(remoteId, "clients");
 
-    Clients.sendCommand(action, args, remoteId);
+    engine.sendCommand(action, args, remoteId);
 
     let newRecord = store._remoteClients[remoteId];
     do_check_neq(newRecord, undefined);
 
     if (expectedResult) {
       _("Ensuring command is sent: " + action);
       do_check_eq(newRecord.commands.length, 1);
 
       let command = newRecord.commands[0];
       do_check_eq(command.command, action);
       do_check_eq(command.args, args);
 
-      do_check_neq(Clients._tracker, undefined);
-      do_check_neq(Clients._tracker.changedIDs[remoteId], undefined);
+      do_check_neq(engine._tracker, undefined);
+      do_check_neq(engine._tracker.changedIDs[remoteId], undefined);
     } else {
       _("Ensuring command is scrubbed: " + action);
       do_check_eq(newRecord.commands, undefined);
 
       if (store._tracker) {
-        do_check_eq(Clients._tracker[remoteId], undefined);
+        do_check_eq(engine._tracker[remoteId], undefined);
       }
     }
 
   }
   run_next_test();
 });
 
 add_test(function test_command_duplication() {
   _("Ensures duplicate commands are detected and not added");
 
-  let store = Clients._store;
+  let store = engine._store;
   let remoteId = Utils.makeGUID();
   let rec = new ClientsRec("clients", remoteId);
   store.create(rec);
   store.createRecord(remoteId, "clients");
 
   let action = "resetAll";
   let args = [];
 
-  Clients.sendCommand(action, args, remoteId);
-  Clients.sendCommand(action, args, remoteId);
+  engine.sendCommand(action, args, remoteId);
+  engine.sendCommand(action, args, remoteId);
 
   let newRecord = store._remoteClients[remoteId];
   do_check_eq(newRecord.commands.length, 1);
 
   _("Check variant args length");
   newRecord.commands = [];
 
   action = "resetEngine";
-  Clients.sendCommand(action, [{ x: "foo" }], remoteId);
-  Clients.sendCommand(action, [{ x: "bar" }], remoteId);
+  engine.sendCommand(action, [{ x: "foo" }], remoteId);
+  engine.sendCommand(action, [{ x: "bar" }], remoteId);
 
   _("Make sure we spot a real dupe argument.");
-  Clients.sendCommand(action, [{ x: "bar" }], remoteId);
+  engine.sendCommand(action, [{ x: "bar" }], remoteId);
 
   do_check_eq(newRecord.commands.length, 2);
 
   run_next_test();
 });
 
 add_test(function test_command_invalid_client() {
   _("Ensures invalid client IDs are caught");
 
   let id = Utils.makeGUID();
   let error;
 
   try {
-    Clients.sendCommand("wipeAll", [], id);
+    engine.sendCommand("wipeAll", [], id);
   } catch (ex) {
     error = ex;
   }
 
   do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
 
   run_next_test();
 });
 
 add_test(function test_process_incoming_commands() {
   _("Ensures local commands are executed");
 
-  Clients.localCommands = [{ command: "logout", args: [] }];
+  engine.localCommands = [{ command: "logout", args: [] }];
 
   let ev = "weave:service:logout:finish";
 
   var handler = function() {
     Svc.Obs.remove(ev, handler);
     run_next_test();
   };
 
   Svc.Obs.add(ev, handler);
 
   // logout command causes processIncomingCommands to return explicit false.
-  do_check_false(Clients.processIncomingCommands());
+  do_check_false(engine.processIncomingCommands());
 });
 
 add_test(function test_command_sync() {
   _("Ensure that commands are synced across clients.");
 
   new SyncTestingInfrastructure();
 
-  Clients._store.wipe();
-  generateNewKeys();
+  engine._store.wipe();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
-    meta: {global: {engines: {clients: {version: Clients.version,
-                                        syncID: Clients.syncID}}}},
+    meta: {global: {engines: {clients: {version: engine.version,
+                                        syncID: engine.syncID}}}},
     clients: {},
     crypto: {}
   };
   let server   = serverForUsers({"foo": "password"}, contents);
   let user     = server.user("foo");
   let remoteId = Utils.makeGUID();
 
   function clientWBO(id) {
     return user.collection("clients").wbo(id);
   }
 
   _("Create remote client record");
   let rec = new ClientsRec("clients", remoteId);
-  Clients._store.create(rec);
-  let remoteRecord = Clients._store.createRecord(remoteId, "clients");
-  Clients.sendCommand("wipeAll", []);
+  engine._store.create(rec);
+  let remoteRecord = engine._store.createRecord(remoteId, "clients");
+  engine.sendCommand("wipeAll", []);
 
-  let clientRecord = Clients._store._remoteClients[remoteId];
+  let clientRecord = engine._store._remoteClients[remoteId];
   do_check_neq(clientRecord, undefined);
   do_check_eq(clientRecord.commands.length, 1);
 
   try {
     _("Syncing.");
-    Clients._sync();
+    engine._sync();
     _("Checking record was uploaded.");
-    do_check_neq(clientWBO(Clients.localID).payload, undefined);
-    do_check_true(Clients.lastRecordUpload > 0);
+    do_check_neq(clientWBO(engine.localID).payload, undefined);
+    do_check_true(engine.lastRecordUpload > 0);
 
     do_check_neq(clientWBO(remoteId).payload, undefined);
 
     Svc.Prefs.set("client.GUID", remoteId);
-    Clients._resetClient();
-    do_check_eq(Clients.localID, remoteId);
+    engine._resetClient();
+    do_check_eq(engine.localID, remoteId);
     _("Performing sync on resetted client.");
-    Clients._sync();
-    do_check_neq(Clients.localCommands, undefined);
-    do_check_eq(Clients.localCommands.length, 1);
+    engine._sync();
+    do_check_neq(engine.localCommands, undefined);
+    do_check_eq(engine.localCommands.length, 1);
 
-    let command = Clients.localCommands[0];
+    let command = engine.localCommands[0];
     do_check_eq(command.command, "wipeAll");
     do_check_eq(command.args.length, 0);
 
   } finally {
     Svc.Prefs.resetBranch("");
-    Records.clearCache();
+    Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_send_uri_to_client_for_display() {
   _("Ensure sendURIToClientForDisplay() sends command properly.");
 
-  let tracker = Clients._tracker;
-  let store = Clients._store;
+  let tracker = engine._tracker;
+  let store = engine._store;
 
   let remoteId = Utils.makeGUID();
   let rec = new ClientsRec("clients", remoteId);
   rec.name = "remote";
   store.create(rec);
   let remoteRecord = store.createRecord(remoteId, "clients");
 
   tracker.clearChangedIDs();
   let initialScore = tracker.score;
 
   let uri = "http://www.mozilla.org/";
   let title = "Title of the Page";
-  Clients.sendURIToClientForDisplay(uri, remoteId, title);
+  engine.sendURIToClientForDisplay(uri, remoteId, title);
 
   let newRecord = store._remoteClients[remoteId];
 
   do_check_neq(newRecord, undefined);
   do_check_eq(newRecord.commands.length, 1);
 
   let command = newRecord.commands[0];
   do_check_eq(command.command, "displayURI");
   do_check_eq(command.args.length, 3);
   do_check_eq(command.args[0], uri);
-  do_check_eq(command.args[1], Clients.localID);
+  do_check_eq(command.args[1], engine.localID);
   do_check_eq(command.args[2], title);
 
   do_check_true(tracker.score > initialScore);
   do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
 
   _("Ensure unknown client IDs result in exception.");
   let unknownId = Utils.makeGUID();
   let error;
 
   try {
-    Clients.sendURIToClientForDisplay(uri, unknownId);
+    engine.sendURIToClientForDisplay(uri, unknownId);
   } catch (ex) {
     error = ex;
   }
 
   do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
 
   run_next_test();
 });
@@ -522,17 +523,17 @@ add_test(function test_receive_display_u
   let remoteId = Utils.makeGUID();
   let title = "Page Title!";
 
   let command = {
     command: "displayURI",
     args: [uri, remoteId, title],
   };
 
-  Clients.localCommands = [command];
+  engine.localCommands = [command];
 
   // Received 'displayURI' command should result in the topic defined below
   // being called.
   let ev = "weave:engine:clients:display-uri";
 
   let handler = function(subject, data) {
     Svc.Obs.remove(ev, handler);
 
@@ -541,16 +542,16 @@ add_test(function test_receive_display_u
     do_check_eq(subject.title, title);
     do_check_eq(data, null);
 
     run_next_test();
   };
 
   Svc.Obs.add(ev, handler);
 
-  do_check_true(Clients.processIncomingCommands());
+  do_check_true(engine.processIncomingCommands());
 });
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Clients").level = Log4Moz.Level.Trace;
   run_next_test();
 }
--- a/services/sync/tests/unit/test_clients_escape.js
+++ b/services/sync/tests/unit/test_clients_escape.js
@@ -1,43 +1,44 @@
 /* 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/clients.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
   _("Set up test fixtures.");
 
-  Identity.username = "john@example.com";
-  Svc.Prefs.set("clusterURL", "http://fakebase/");
+  Service.identity.username = "john@example.com";
+  Service.clusterURL = "http://fakebase/";
   let baseUri = "http://fakebase/1.1/foo/storage/";
   let pubUri = baseUri + "keys/pubkey";
   let privUri = baseUri + "keys/privkey";
 
-  Identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
-  let keyBundle = Identity.syncKeyBundle;
+  Service.identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
+  let keyBundle = Service.identity.syncKeyBundle;
+
+  let engine = Service.clientsEngine;
 
   try {
     _("Test that serializing client records results in uploadable ascii");
-    Clients.localID = "ascii";
-    Clients.localName = "wéävê";
+    engine.localID = "ascii";
+    engine.localName = "wéävê";
 
     _("Make sure we have the expected record");
-    let record = Clients._createRecord("ascii");
+    let record = engine._createRecord("ascii");
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
 
     _("Encrypting record...");
     record.encrypt(keyBundle);
     _("Encrypted.");
-    
+
     let serialized = JSON.stringify(record);
     let checkCount = 0;
     _("Checking for all ASCII:", serialized);
     Array.forEach(serialized, function(ch) {
       let code = ch.charCodeAt(0);
       _("Checking asciiness of '", ch, "'=", code);
       do_check_true(code < 128);
       checkCount++;
@@ -47,15 +48,15 @@ function run_test() {
     do_check_eq(checkCount, serialized.length);
 
     _("Making sure the record still looks like it did before");
     record.decrypt(keyBundle);
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
 
     _("Sanity check that creating the record also gives the same");
-    record = Clients._createRecord("ascii");
+    record = engine._createRecord("ascii");
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
   } finally {
     Svc.Prefs.resetBranch("");
   }
 }
--- a/services/sync/tests/unit/test_collection_inc_get.js
+++ b/services/sync/tests/unit/test_collection_inc_get.js
@@ -1,14 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Make sure Collection can correctly incrementally parse GET requests");
 Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/service.js");
 
 function run_test() {
   let base = "http://fake/";
-  let coll = new Collection("http://fake/uri/", WBORecord);
+  let coll = new Collection("http://fake/uri/", WBORecord, Service);
   let stream = { _data: "" };
   let called, recCount, sum;
 
   _("Not-JSON, string payloads are strings");
   called = false;
   stream._data = '{"id":"hello","payload":"world"}\n';
   coll.recordHandler = function(rec) {
     called = true;
--- a/services/sync/tests/unit/test_collections_recovery.js
+++ b/services/sync/tests/unit/test_collections_recovery.js
@@ -1,16 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 // Verify that we wipe the server if we have to regenerate keys.
 Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 add_test(function test_missing_crypto_collection() {
   let johnHelper = track_collections_helper();
   let johnU      = johnHelper.with_updated_collection;
   let johnColls  = johnHelper.collections;
-  
+
   let empty = false;
   function maybe_empty(handler) {
     return function (request, response) {
       if (empty) {
         let body = "{}";
         response.setStatusLine(request.httpVersion, 200, "OK");
         response.bodyOutputStream.write(body, body.length);
       } else {
@@ -39,17 +44,17 @@ add_test(function test_missing_crypto_co
   try {
     let fresh = 0;
     let orig  = Service._freshStart;
     Service._freshStart = function() {
       _("Called _freshStart.");
       orig.call(Service);
       fresh++;
     };
-    
+
     _("Startup, no meta/global: freshStart called once.");
     Service.sync();
     do_check_eq(fresh, 1);
     fresh = 0;
 
     _("Regular sync: no need to freshStart.");
     Service.sync();
     do_check_eq(fresh, 0);
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -1,45 +1,48 @@
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://services-sync/service.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-sync/constants.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-common/log4moz.js");
-  
+Cu.import("resource://services-sync/record.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://testing-common/services/sync/utils.js");
+
 add_test(function test_locally_changed_keys() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let hmacErrorCount = 0;
   function counting(f) {
     return function() {
       hmacErrorCount++;
       return f.call(this);
     };
   }
-  
-  Weave.Service.handleHMACEvent = counting(Weave.Service.handleHMACEvent);
-  
+
+  Service.handleHMACEvent = counting(Service.handleHMACEvent);
+
   let server  = new SyncServer();
   let johndoe = server.registerUser("johndoe", "password");
   johndoe.createContents({
     meta: {},
     crypto: {},
     clients: {}
   });
   server.start();
 
   try {
     Svc.Prefs.set("registerEngines", "Tab");
     _("Set up some tabs.");
-    let myTabs = 
+    let myTabs =
       {windows: [{tabs: [{index: 1,
                           entries: [{
                             url: "http://foo.com/",
                             title: "Title"
                           }],
                           attributes: {
                             image: "image"
                           },
@@ -50,157 +53,156 @@ add_test(function test_locally_changed_k
     Svc.Session = {
       getBrowserState: function () JSON.stringify(myTabs)
     };
 
     setBasicCredentials("johndoe", "password", passphrase);
     Service.serverURL = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
 
-    Engines.register(HistoryEngine);
-    Weave.Service._registerEngines();
-    
+    Service.engineManager.register(HistoryEngine);
+
     function corrupt_local_keys() {
-      CollectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
-                                         Svc.Crypto.generateRandomKey()];
+      Service.collectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
+                                                 Svc.Crypto.generateRandomKey()];
     }
-    
+
     _("Setting meta.");
-    
+
     // Bump version on the server.
     let m = new WBORecord("meta", "global");
     m.payload = {"syncID": "foooooooooooooooooooooooooo",
                  "storageVersion": STORAGE_VERSION};
-    m.upload(Weave.Service.metaURL);
-    
+    m.upload(Service.resource(Service.metaURL));
+
     _("New meta/global: " + JSON.stringify(johndoe.collection("meta").wbo("global")));
-    
+
     // Upload keys.
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
-    do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
-    
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+    serverKeys.encrypt(Service.identity.syncKeyBundle);
+    do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
+
     // Check that login works.
-    do_check_true(Weave.Service.login("johndoe", "ilovejane", passphrase));
-    do_check_true(Weave.Service.isLoggedIn);
-    
+    do_check_true(Service.login("johndoe", "ilovejane", passphrase));
+    do_check_true(Service.isLoggedIn);
+
     // Sync should upload records.
-    Weave.Service.sync();
-    
+    Service.sync();
+
     // Tabs exist.
     _("Tabs modified: " + johndoe.modified("tabs"));
     do_check_true(johndoe.modified("tabs") > 0);
-    
-    let coll_modified = CollectionKeys.lastModified;
-    
+
+    let coll_modified = Service.collectionKeys.lastModified;
+
     // Let's create some server side history records.
-    let liveKeys = CollectionKeys.keyForCollection("history");
+    let liveKeys = Service.collectionKeys.keyForCollection("history");
     _("Keys now: " + liveKeys.keyPair);
     let visitType = Ci.nsINavHistoryService.TRANSITION_LINK;
     let history   = johndoe.createCollection("history");
     for (let i = 0; i < 5; i++) {
       let id = 'record-no--' + i;
       let modified = Date.now()/1000 - 60*(i+10);
-      
+
       let w = new CryptoWrapper("history", "id");
       w.cleartext = {
         id: id,
         histUri: "http://foo/bar?" + id,
         title: id,
         sortindex: i,
         visits: [{date: (modified - 5) * 1000000, type: visitType}],
         deleted: false};
-      w.encrypt();
-      
+      w.encrypt(liveKeys);
+
       let payload = {ciphertext: w.ciphertext,
                      IV:         w.IV,
                      hmac:       w.hmac};
       history.insert(id, payload, modified);
     }
-    
+
     history.timestamp = Date.now() / 1000;
     let old_key_time = johndoe.modified("crypto");
     _("Old key time: " + old_key_time);
-    
+
     // Check that we can decrypt one.
     let rec = new CryptoWrapper("history", "record-no--0");
-    rec.fetch(Weave.Service.storageURL + "history/record-no--0");
+    rec.fetch(Service.resource(Service.storageURL + "history/record-no--0"));
     _(JSON.stringify(rec));
-    do_check_true(!!rec.decrypt());
-    
+    do_check_true(!!rec.decrypt(liveKeys));
+
     do_check_eq(hmacErrorCount, 0);
-    
+
     // Fill local key cache with bad data.
     corrupt_local_keys();
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
-    
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
+
     do_check_eq(hmacErrorCount, 0);
-    
+
     _("HMAC error count: " + hmacErrorCount);
     // Now syncing should succeed, after one HMAC error.
-    Weave.Service.sync();
+    Service.sync();
     do_check_eq(hmacErrorCount, 1);
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
-    
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
+
     // And look! We downloaded history!
-    let store = Engines.get("history")._store;
+    let store = Service.engineManager.get("history")._store;
     do_check_true(store.urlExists("http://foo/bar?record-no--0"));
     do_check_true(store.urlExists("http://foo/bar?record-no--1"));
     do_check_true(store.urlExists("http://foo/bar?record-no--2"));
     do_check_true(store.urlExists("http://foo/bar?record-no--3"));
     do_check_true(store.urlExists("http://foo/bar?record-no--4"));
     do_check_eq(hmacErrorCount, 1);
-    
+
     _("Busting some new server values.");
     // Now what happens if we corrupt the HMAC on the server?
     for (let i = 5; i < 10; i++) {
       let id = 'record-no--' + i;
       let modified = 1 + (Date.now() / 1000);
-      
+
       let w = new CryptoWrapper("history", "id");
       w.cleartext = {
         id: id,
         histUri: "http://foo/bar?" + id,
         title: id,
         sortindex: i,
         visits: [{date: (modified - 5 ) * 1000000, type: visitType}],
         deleted: false};
-      w.encrypt();
+      w.encrypt(Service.collectionKeys.keyForCollection("history"));
       w.hmac = w.hmac.toUpperCase();
-      
+
       let payload = {ciphertext: w.ciphertext,
                      IV:         w.IV,
                      hmac:       w.hmac};
       history.insert(id, payload, modified);
     }
     history.timestamp = Date.now() / 1000;
-    
+
     _("Server key time hasn't changed.");
     do_check_eq(johndoe.modified("crypto"), old_key_time);
-    
+
     _("Resetting HMAC error timer.");
-    Weave.Service.lastHMACEvent = 0;
-    
+    Service.lastHMACEvent = 0;
+
     _("Syncing...");
-    Weave.Service.sync();
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
+    Service.sync();
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
     _("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history.");
     do_check_true(johndoe.modified("crypto") > old_key_time);
     do_check_eq(hmacErrorCount, 6);
     do_check_false(store.urlExists("http://foo/bar?record-no--5"));
     do_check_false(store.urlExists("http://foo/bar?record-no--6"));
     do_check_false(store.urlExists("http://foo/bar?record-no--7"));
     do_check_false(store.urlExists("http://foo/bar?record-no--8"));
     do_check_false(store.urlExists("http://foo/bar?record-no--9"));
-    
+
   } finally {
-    Weave.Svc.Prefs.resetBranch("");
+    Svc.Prefs.resetBranch("");
     server.stop(run_next_test);
   }
 });
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
-  
+
   run_next_test();
 }
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -1,34 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 
-function SteamStore() {
-  Store.call(this, "Steam");
+function SteamStore(engine) {
+  Store.call(this, "Steam", engine);
   this.wasWiped = false;
 }
 SteamStore.prototype = {
   __proto__: Store.prototype,
 
   wipe: function() {
     this.wasWiped = true;
   }
 };
 
-function SteamTracker() {
-  Tracker.call(this, "Steam");
+function SteamTracker(engine) {
+  Tracker.call(this, "Steam", engine);
 }
 SteamTracker.prototype = {
   __proto__: Tracker.prototype
 };
 
 function SteamEngine() {
-  Engine.call(this, "Steam");
+  Engine.call(this, "Steam", Service);
   this.wasReset = false;
   this.wasSynced = false;
 }
 SteamEngine.prototype = {
   __proto__: Engine.prototype,
   _storeObj: SteamStore,
   _trackerObj: SteamTracker,
 
@@ -58,57 +62,57 @@ Observers.add("weave:engine:reset-client
 Observers.add("weave:engine:wipe-client:start", engineObserver);
 Observers.add("weave:engine:wipe-client:finish", engineObserver);
 Observers.add("weave:engine:sync:start", engineObserver);
 Observers.add("weave:engine:sync:finish", engineObserver);
 
 
 function test_members() {
   _("Engine object members");
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   do_check_eq(engine.Name, "Steam");
   do_check_eq(engine.prefName, "steam");
   do_check_true(engine._store instanceof SteamStore);
   do_check_true(engine._tracker instanceof SteamTracker);
 }
 
 function test_score() {
   _("Engine.score corresponds to tracker.score and is readonly");
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   do_check_eq(engine.score, 0);
   engine._tracker.score += 5;
   do_check_eq(engine.score, 5);
 
   try {
     engine.score = 10;
   } catch(ex) {
     // Setting an attribute that has a getter produces an error in
     // Firefox <= 3.6 and is ignored in later versions.  Either way,
     // the attribute's value won't change.
   }
   do_check_eq(engine.score, 5);
 }
 
 function test_resetClient() {
   _("Engine.resetClient calls _resetClient");
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   do_check_false(engine.wasReset);
 
   engine.resetClient();
   do_check_true(engine.wasReset);
   do_check_eq(engineObserver.topics[0], "weave:engine:reset-client:start");
   do_check_eq(engineObserver.topics[1], "weave:engine:reset-client:finish");
 
   engine.wasReset = false;
   engineObserver.reset();
 }
 
 function test_wipeClient() {
   _("Engine.wipeClient calls resetClient, wipes store, clears changed IDs");
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   do_check_false(engine.wasReset);
   do_check_false(engine._store.wasWiped);
   do_check_true(engine._tracker.addChangedID("a-changed-id"));
   do_check_true("a-changed-id" in engine._tracker.changedIDs);
 
   engine.wipeClient();
   do_check_true(engine.wasReset);
   do_check_true(engine._store.wasWiped);
@@ -120,31 +124,31 @@ function test_wipeClient() {
 
   engine.wasReset = false;
   engine._store.wasWiped = false;
   engineObserver.reset();
 }
 
 function test_enabled() {
   _("Engine.enabled corresponds to preference");
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   try {
     do_check_false(engine.enabled);
     Svc.Prefs.set("engine.steam", true);
     do_check_true(engine.enabled);
 
     engine.enabled = false;
     do_check_false(Svc.Prefs.get("engine.steam"));
   } finally {
     Svc.Prefs.resetBranch("");
   }
 }
 
 function test_sync() {
-  let engine = new SteamEngine();
+  let engine = new SteamEngine(Service);
   try {
     _("Engine.sync doesn't call _sync if it's not enabled");
     do_check_false(engine.enabled);
     do_check_false(engine.wasSynced);
     engine.sync();
     do_check_false(engine.wasSynced);
 
     _("Engine.sync calls _sync if it's enabled");
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -1,20 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/rotaryengine.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 add_test(function test_processIncoming_abort() {
   _("An abort exception, raised in applyIncoming, will abort _processIncoming.");
   new SyncTestingInfrastructure();
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
-  let engine = new RotaryEngine();
+  let engine = new RotaryEngine(Service);
 
   _("Create some server data.");
-  let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
+  let meta_global = Service.recordManager.set(engine.metaURL,
+                                              new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                           syncID: engine.syncID}};
 
   let collection = new ServerCollection();
   let id = Utils.makeGUID();
   let payload = encryptPayload({id: id, denomination: "Record No. " + id});
   collection.insert(id, payload);
 
@@ -24,17 +32,17 @@ add_test(function test_processIncoming_a
 
   _("Fake applyIncoming to abort.");
   engine._store.applyIncoming = function (record) {
     let ex = {code: Engine.prototype.eEngineAbortApplyIncoming,
               cause: "Nooo"};
     _("Throwing: " + JSON.stringify(ex));
     throw ex;
   };
-  
+
   _("Trying _processIncoming. It will throw after aborting.");
   let err;
   try {
     engine._syncStartup();
     engine._processIncoming();
   } catch (ex) {
     err = ex;
   }
@@ -49,14 +57,14 @@ add_test(function test_processIncoming_a
   } catch (ex) {
     err = ex;
   }
 
   do_check_eq(err, undefined);
 
   server.stop(run_next_test);
   Svc.Prefs.resetBranch("");
-  Records.clearCache();
+  Service.recordManager.clearCache();
 });
 
 function run_test() {
   run_next_test();
 }
--- a/services/sync/tests/unit/test_enginemanager.js
+++ b/services/sync/tests/unit/test_enginemanager.js
@@ -1,80 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/service.js");
 
 function run_test() {
   _("We start out with a clean slate");
-  let engines = Engines.getAll();
+
+  let manager = new EngineManager(Service);
+
+  let engines = manager.getAll();
   do_check_eq(engines.length, 0);
-  do_check_eq(Engines.get('dummy'), undefined);
+  do_check_eq(manager.get('dummy'), undefined);
 
   _("Register an engine");
   function DummyEngine() {}
   DummyEngine.prototype.name = "dummy";
-  Engines.register(DummyEngine);
-  let dummy = Engines.get('dummy');
+  manager.register(DummyEngine);
+  let dummy = manager.get('dummy');
   do_check_true(dummy instanceof DummyEngine);
 
-  engines = Engines.getAll();
+  engines = manager.getAll();
   do_check_eq(engines.length, 1);
   do_check_eq(engines[0], dummy);
 
   _("Register an already registered engine is ignored");
-  Engines.register(DummyEngine);
-  do_check_eq(Engines.get('dummy'), dummy);
+  manager.register(DummyEngine);
+  do_check_eq(manager.get('dummy'), dummy);
 
   _("Register multiple engines in one go");
   function PetrolEngine() {}
   PetrolEngine.prototype.name = "petrol";
   function DieselEngine() {}
   DieselEngine.prototype.name = "diesel";
 
-  Engines.register([PetrolEngine, DieselEngine]);
-  let petrol = Engines.get('petrol');
-  let diesel = Engines.get('diesel');
+  manager.register([PetrolEngine, DieselEngine]);
+  let petrol = manager.get('petrol');
+  let diesel = manager.get('diesel');
   do_check_true(petrol instanceof PetrolEngine);
   do_check_true(diesel instanceof DieselEngine);
 
-  engines = Engines.getAll();
+  engines = manager.getAll();
   do_check_eq(engines.length, 3);
   do_check_neq(engines.indexOf(petrol), -1);
   do_check_neq(engines.indexOf(diesel), -1);
 
   _("Retrieve multiple engines in one go");
-  engines = Engines.get(["dummy", "diesel"]);
+  engines = manager.get(["dummy", "diesel"]);
   do_check_eq(engines.length, 2);
   do_check_neq(engines.indexOf(dummy), -1);
   do_check_neq(engines.indexOf(diesel), -1);
 
   _("getEnabled() only returns enabled engines");
-  engines = Engines.getEnabled();
+  engines = manager.getEnabled();
   do_check_eq(engines.length, 0);
 
   petrol.enabled = true;
-  engines = Engines.getEnabled();
+  engines = manager.getEnabled();
   do_check_eq(engines.length, 1);
   do_check_eq(engines[0], petrol);
 
   dummy.enabled = true;
   diesel.enabled = true;
-  engines = Engines.getEnabled();
+  engines = manager.getEnabled();
   do_check_eq(engines.length, 3);
 
   _("Unregister an engine by name");
-  Engines.unregister('dummy');
-  do_check_eq(Engines.get('dummy'), undefined);
-  engines = Engines.getAll();
+  manager.unregister('dummy');
+  do_check_eq(manager.get('dummy'), undefined);
+  engines = manager.getAll();
   do_check_eq(engines.length, 2);
   do_check_eq(engines.indexOf(dummy), -1);
 
   _("Unregister an engine by value");
-  // Engines.unregister() checks for instanceof Engine, so let's make one:
+  // manager.unregister() checks for instanceof Engine, so let's make one:
   function ActualEngine() {}
   ActualEngine.prototype = {__proto__: Engine.prototype,
                             name: 'actual'};
-  Engines.register(ActualEngine);
-  let actual = Engines.get('actual');
+  manager.register(ActualEngine);
+  let actual = manager.get('actual');
   do_check_true(actual instanceof ActualEngine);
   do_check_true(actual instanceof Engine);
 
-  Engines.unregister(actual);
-  do_check_eq(Engines.get('actual'), undefined);
+  manager.unregister(actual);
+  do_check_eq(manager.get('actual'), undefined);
 }
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -1,84 +1,92 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
-
-Svc.DefaultPrefs.set("registerEngines", "");
-Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
 
 const TEST_MAINTENANCE_URL = "http://localhost:8080/maintenance/";
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 
 const PROLONGED_ERROR_DURATION =
   (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
 
 const NON_PROLONGED_ERROR_DURATION =
   (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
 
+Service.engineManager.clear();
+
 function setLastSync(lastSyncValue) {
   Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
 }
 
 function CatapultEngine() {
-  SyncEngine.call(this, "Catapult");
+  SyncEngine.call(this, "Catapult", Service);
 }
 CatapultEngine.prototype = {
   __proto__: SyncEngine.prototype,
   exception: null, // tests fill this in
   _sync: function _sync() {
     if (this.exception) {
       throw this.exception;
     }
   }
 };
 
-Engines.register(CatapultEngine);
+let engineManager = Service.engineManager;
+engineManager.register(CatapultEngine);
+
+// This relies on Service/ErrorHandler being a singleton. Fixing this will take
+// a lot of work.
+let errorHandler = Service.errorHandler;
 
 function run_test() {
   initTestLogging("Trace");
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
 
   run_next_test();
 }
 
 function generateCredentialsChangedFailure() {
   // Make sync fail due to changed credentials. We simply re-encrypt
   // the keys with a different Sync Key, without changing the local one.
   let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
-  let keys = CollectionKeys.asWBO();
+  let keys = Service.collectionKeys.asWBO();
   keys.encrypt(newSyncKeyBundle);
-  keys.upload(Service.cryptoKeysURL);
+  keys.upload(Service.resource(Service.cryptoKeysURL));
 }
 
 function service_unavailable(request, response) {
   let body = "Service Unavailable";
   response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   response.setHeader("Retry-After", "42");
   response.bodyOutputStream.write(body, body.length);
 }
 
 function sync_httpd_setup() {
   let global = new ServerWBO("global", {
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
-    engines: {clients: {version: Clients.version,
-                        syncID: Clients.syncID},
-              catapult: {version: Engines.get("catapult").version,
-                         syncID: Engines.get("catapult").syncID}}
+    engines: {clients: {version: Service.clientsEngine.version,
+                        syncID: Service.clientsEngine.syncID},
+              catapult: {version: engineManager.get("catapult").version,
+                         syncID: engineManager.get("catapult").syncID}}
   });
   let clientsColl = new ServerCollection({}, true);
 
   // Tracking info/collections.
   let collectionsHelper = track_collections_helper();
   let upd = collectionsHelper.with_updated_collection;
 
   let handler_401 = httpd_handler(401, "Unauthorized");
@@ -121,20 +129,20 @@ function setUp() {
   setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   return generateAndUploadKeys();
 }
 
 function generateAndUploadKeys() {
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Identity.syncKeyBundle);
-  return serverKeys.upload(Service.cryptoKeysURL).success;
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+  serverKeys.encrypt(Service.identity.syncKeyBundle);
+  return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
 }
 
 function clean() {
   Service.startOver();
   Status.resetSync();
   Status.resetBackoff();
 }
 
@@ -196,207 +204,207 @@ add_test(function test_credentials_chang
   // Clean up.
   Service.startOver();
   server.stop(run_next_test);
 });
 
 add_test(function test_no_lastSync_pref() {
   // Test reported error.
   Status.resetSync();
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test unreported error.
   Status.resetSync();
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   run_next_test();
 });
 
 add_test(function test_shouldReportError() {
   Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Give ourselves a clusterURL so that the temporary 401 no-error situation
   // doesn't come into play.
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, non-network, prolonged, login error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, non-network, prolonged, sync error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, network, non-prolonged, login error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, network, non-prolonged, sync error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, network, prolonged, login error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, network, prolonged, sync error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test non-network, prolonged, login error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test non-network, prolonged, sync error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test network, prolonged, login error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test network, prolonged, sync error reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test non-network, non-prolonged, login error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test non-network, non-prolonged, sync error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test network, non-prolonged, login error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Test network, non-prolonged, sync error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Test server maintenance, sync errors are not reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = SERVER_MAINTENANCE;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Test server maintenance, login errors are not reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = SERVER_MAINTENANCE;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Test prolonged, server maintenance, sync errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.sync = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test prolonged, server maintenance, login errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = false;
+  errorHandler.dontIgnoreErrors = false;
   Status.login = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, server maintenance, sync errors are reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, server maintenance, login errors are reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, prolonged, server maintenance,
   // sync errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.sync = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   // Test dontIgnoreErrors, prolonged, server maintenance,
   // login errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.dontIgnoreErrors = true;
+  errorHandler.dontIgnoreErrors = true;
   Status.login = SERVER_MAINTENANCE;
-  do_check_true(ErrorHandler.shouldReportError());
+  do_check_true(errorHandler.shouldReportError());
 
   run_next_test();
 });
 
 add_test(function test_shouldReportError_master_password() {
   _("Test error ignored due to locked master password");
   let server = sync_httpd_setup();
   setUp();
@@ -406,41 +414,41 @@ add_test(function test_shouldReportError
   Service._verifyLogin = Service.verifyLogin;
   Service.verifyLogin = function () {
     Status.login = MASTER_PASSWORD_LOCKED;
     return false;
   };
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   // Clean up.
   Service.verifyLogin = Service._verifyLogin;
   clean();
   server.stop(run_next_test);
 });
 
 add_test(function test_login_syncAndReportErrors_non_network_error() {
   // Test non-network errors are reported
   // when calling syncAndReportErrors
   let server = sync_httpd_setup();
   setUp();
-  Identity.basicPassword = null;
+  Service.identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_non_network_error() {
   // Test non-network errors are reported
   // when calling syncAndReportErrors
   let server = sync_httpd_setup();
   setUp();
 
@@ -455,36 +463,36 @@ add_test(function test_sync_syncAndRepor
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
 
     clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_non_network_error() {
   // Test prolonged, non-network errors are
   // reported when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
-  Identity.basicPassword = null;
+  Service.identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_prolonged_non_network_error() {
   // Test prolonged, non-network errors are
   // reported when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
@@ -499,17 +507,17 @@ add_test(function test_sync_syncAndRepor
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
 
     clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_network_error() {
   // Test network errors are reported when calling syncAndReportErrors.
   setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
@@ -517,17 +525,17 @@ add_test(function test_login_syncAndRepo
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
     clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 
 add_test(function test_sync_syncAndReportErrors_network_error() {
   // Test network errors are reported when calling syncAndReportErrors.
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
@@ -535,17 +543,17 @@ add_test(function test_sync_syncAndRepor
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
 
     Services.io.offline = false;
     clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_network_error() {
   // Test prolonged, network errors are reported
   // when calling syncAndReportErrors.
   setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
@@ -554,17 +562,17 @@ add_test(function test_login_syncAndRepo
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
     clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
   // Test prolonged, network errors are reported
   // when calling syncAndReportErrors.
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
@@ -572,24 +580,24 @@ add_test(function test_sync_syncAndRepor
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
 
     Services.io.offline = false;
     clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_prolonged_non_network_error() {
   // Test prolonged, non-network errors are reported
   let server = sync_httpd_setup();
   setUp();
-  Identity.basicPassword = null;
+  Service.identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
     clean();
     server.stop(run_next_test);
   });
@@ -656,17 +664,17 @@ add_test(function test_sync_prolonged_ne
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_non_network_error() {
   // Test non-network errors are reported
   let server = sync_httpd_setup();
   setUp();
-  Identity.basicPassword = null;
+  Service.identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
@@ -737,17 +745,17 @@ add_test(function test_sync_network_erro
 });
 
 add_test(function test_sync_server_maintenance_error() {
   // Test server maintenance errors are not reported.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   function onSyncError() {
     do_throw("Shouldn't get here!");
   }
   Svc.Obs.add("weave:ui:sync:error", onSyncError);
@@ -855,17 +863,17 @@ add_test(function test_crypto_keys_login
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   function onUIUpdate() {
@@ -894,17 +902,17 @@ add_test(function test_crypto_keys_login
 });
 
 add_test(function test_sync_prolonged_server_maintenance_error() {
   // Test prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
@@ -989,17 +997,17 @@ add_test(function test_download_crypto_k
   // Test crypto/keys prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
@@ -1093,17 +1101,17 @@ add_test(function test_wipeRemote_prolon
   let server = sync_httpd_setup();
 
   server.registerPathHandler("/1.1/broken.wipe/storage/catapult", service_unavailable);
   setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   generateAndUploadKeys();
 
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.exception = null;
   engine.enabled = true;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1130,34 +1138,34 @@ add_test(function test_wipeRemote_prolon
 
 add_test(function test_sync_syncAndReportErrors_server_maintenance_error() {
   // Test server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
     do_check_eq(Status.sync, SERVER_MAINTENANCE);
 
     clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
   // Test info/collections server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
@@ -1181,17 +1189,17 @@ add_test(function test_info_collections_
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
   // Test meta/global server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
@@ -1215,30 +1223,30 @@ add_test(function test_meta_global_login
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
@@ -1251,17 +1259,17 @@ add_test(function test_download_crypto_k
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
@@ -1285,17 +1293,17 @@ add_test(function test_upload_crypto_key
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
@@ -1319,30 +1327,30 @@ add_test(function test_wipeServer_login_
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
   // Test that we report prolonged server maintenance errors that occur whilst
   // wiping all remote devices.
   let server = sync_httpd_setup();
 
   setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   generateAndUploadKeys();
 
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.exception = null;
   engine.enabled = true;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1359,44 +1367,44 @@ add_test(function test_wipeRemote_syncAn
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   Svc.Prefs.set("firstSync", "wipeRemote");
   setLastSync(NON_PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test prolonged server maintenance errors are
   // reported when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
     do_check_eq(Status.sync, SERVER_MAINTENANCE);
 
     clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test info/collections server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
@@ -1420,17 +1428,17 @@ add_test(function test_info_collections_
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test meta/global server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
@@ -1454,30 +1462,30 @@ add_test(function test_meta_global_login
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
@@ -1490,17 +1498,17 @@ add_test(function test_download_crypto_k
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
@@ -1524,17 +1532,17 @@ add_test(function test_upload_crypto_key
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
@@ -1558,23 +1566,23 @@ add_test(function test_wipeServer_login_
     clean();
     server.stop(run_next_test);
   });
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
-  ErrorHandler.syncAndReportErrors();
+  errorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_engine_generic_fail() {
   let server = sync_httpd_setup();
 
-  let engine = Engines.get("catapult");
+  let engine = engineManager.get("catapult");
   engine.enabled = true;
   engine.sync = function sync() {
     Svc.Obs.notify("weave:engine:sync:error", "", "catapult");
   };
 
   let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
   Svc.Prefs.set("log.appender.file.logOnError", true);
 
@@ -1616,17 +1624,17 @@ add_test(function test_logs_on_sync_erro
     "is notified, despite shouldReportError returning false.");
 
   let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
   Svc.Prefs.set("log.appender.file.logOnError", true);
   log.info("TESTING");
 
   // Ensure that we report no error.
   Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());
 
   Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
     Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
 
     // Test that error log was written.
     let entries = logsdir.directoryEntries;
     do_check_true(entries.hasMoreElements());
     let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
@@ -1644,17 +1652,17 @@ add_test(function test_logs_on_login_err
     "is notified, despite shouldReportError returning false.");
 
   let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
   Svc.Prefs.set("log.appender.file.logOnError", true);
   log.info("TESTING");
 
   // Ensure that we report no error.
   Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(ErrorHandler.shouldReportError());
+  do_check_false(errorHandler.shouldReportError());