forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			237 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(
 | |
|   lazy,
 | |
|   "IndexedDB",
 | |
|   "resource://gre/modules/IndexedDB.jsm"
 | |
| );
 | |
| ChromeUtils.defineModuleGetter(
 | |
|   lazy,
 | |
|   "TelemetryEnvironment",
 | |
|   "resource://gre/modules/TelemetryEnvironment.jsm"
 | |
| );
 | |
| ChromeUtils.defineModuleGetter(
 | |
|   lazy,
 | |
|   "TelemetryEvents",
 | |
|   "resource://normandy/lib/TelemetryEvents.jsm"
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * AddonRollouts store info about an active or expired addon rollouts.
 | |
|  * @typedef {object} AddonRollout
 | |
|  * @property {int} recipeId
 | |
|  *   The ID of the recipe.
 | |
|  * @property {string} slug
 | |
|  *   Unique slug of the rollout.
 | |
|  * @property {string} state
 | |
|  *   The current state of the rollout: "active", or "rolled-back".
 | |
|  *   Active means that Normandy is actively managing therollout. Rolled-back
 | |
|  *   means that the rollout was previously active, but has been rolled back for
 | |
|  *   this user.
 | |
|  * @property {int} extensionApiId
 | |
|  *   The ID used to look up the extension in Normandy's API.
 | |
|  * @property {string} addonId
 | |
|  *   The add-on ID for this particular rollout.
 | |
|  * @property {string} addonVersion
 | |
|  *   The rollout add-on version number
 | |
|  * @property {string} xpiUrl
 | |
|  *   URL that the add-on was installed from.
 | |
|  * @property {string} xpiHash
 | |
|  *   The hash of the XPI file.
 | |
|  * @property {string} xpiHashAlgorithm
 | |
|  *   The algorithm used to hash the XPI file.
 | |
|  * @property {string} enrollmentId
 | |
|  *   A random ID generated at time of enrollment. It should be included on all
 | |
|  *   telemetry related to this rollout. It should not be re-used by other
 | |
|  *   rollouts, or any other purpose. May be null on old rollouts.
 | |
|  */
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ["AddonRollouts"];
 | |
| const DB_NAME = "normandy-addon-rollout";
 | |
| const STORE_NAME = "addon-rollouts";
 | |
| const DB_OPTIONS = { version: 1 };
 | |
| 
 | |
| /**
 | |
|  * Create a new connection to the database.
 | |
|  */
 | |
| function openDatabase() {
 | |
|   return lazy.IndexedDB.open(DB_NAME, DB_OPTIONS, db => {
 | |
|     db.createObjectStore(STORE_NAME, {
 | |
|       keyPath: "slug",
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Cache the database connection so that it is shared among multiple operations.
 | |
|  */
 | |
| let databasePromise;
 | |
| function getDatabase() {
 | |
|   if (!databasePromise) {
 | |
|     databasePromise = openDatabase();
 | |
|   }
 | |
|   return databasePromise;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get a transaction for interacting with the rollout store.
 | |
|  *
 | |
|  * @param {IDBDatabase} db
 | |
|  * @param {String} mode Either "readonly" or "readwrite"
 | |
|  *
 | |
|  * NOTE: Methods on the store returned by this function MUST be called
 | |
|  * synchronously, otherwise the transaction with the store will expire.
 | |
|  * This is why the helper takes a database as an argument; if we fetched the
 | |
|  * database in the helper directly, the helper would be async and the
 | |
|  * transaction would expire before methods on the store were called.
 | |
|  */
 | |
| function getStore(db, mode) {
 | |
|   if (!mode) {
 | |
|     throw new Error("mode is required");
 | |
|   }
 | |
|   return db.objectStore(STORE_NAME, mode);
 | |
| }
 | |
| 
 | |
| const AddonRollouts = {
 | |
|   STATE_ACTIVE: "active",
 | |
|   STATE_ROLLED_BACK: "rolled-back",
 | |
| 
 | |
|   async init() {
 | |
|     for (const rollout of await this.getAllActive()) {
 | |
|       lazy.TelemetryEnvironment.setExperimentActive(
 | |
|         rollout.slug,
 | |
|         rollout.state,
 | |
|         {
 | |
|           type: "normandy-addonrollout",
 | |
|         }
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /** When Telemetry is disabled, clear all identifiers from the stored rollouts.  */
 | |
|   async onTelemetryDisabled() {
 | |
|     const rollouts = await this.getAll();
 | |
|     for (const rollout of rollouts) {
 | |
|       rollout.enrollmentId = lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER;
 | |
|     }
 | |
|     await this.updateMany(rollouts);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a new rollout
 | |
|    * @param {AddonRollout} rollout
 | |
|    */
 | |
|   async add(rollout) {
 | |
|     const db = await getDatabase();
 | |
|     return getStore(db, "readwrite").add(rollout);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update an existing rollout
 | |
|    * @param {AddonRollout} rollout
 | |
|    * @throws If a matching rollout does not exist.
 | |
|    */
 | |
|   async update(rollout) {
 | |
|     if (!(await this.has(rollout.slug))) {
 | |
|       throw new Error(
 | |
|         `Tried to update ${rollout.slug}, but it doesn't already exist.`
 | |
|       );
 | |
|     }
 | |
|     const db = await getDatabase();
 | |
|     return getStore(db, "readwrite").put(rollout);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update many existing rollouts. More efficient than calling `update` many
 | |
|    * times in a row.
 | |
|    * @param {Array<PreferenceRollout>} rollouts
 | |
|    * @throws If any of the passed rollouts have a slug that doesn't exist in the database already.
 | |
|    */
 | |
|   async updateMany(rollouts) {
 | |
|     // Don't touch the database if there is nothing to do
 | |
|     if (!rollouts.length) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Both of the below operations use .map() instead of a normal loop becaues
 | |
|     // once we get the object store, we can't let it expire by spinning the
 | |
|     // event loop. This approach queues up all the interactions with the store
 | |
|     // immediately, preventing it from expiring too soon.
 | |
| 
 | |
|     const db = await getDatabase();
 | |
|     let store = await getStore(db, "readonly");
 | |
|     await Promise.all(
 | |
|       rollouts.map(async ({ slug }) => {
 | |
|         let existingRollout = await store.get(slug);
 | |
|         if (!existingRollout) {
 | |
|           throw new Error(`Tried to update ${slug}, but it doesn't exist.`);
 | |
|         }
 | |
|       })
 | |
|     );
 | |
| 
 | |
|     // awaiting spun the event loop, so the store is now invalid. Get a new
 | |
|     // store. This is also a chance to get it in readwrite mode.
 | |
|     store = await getStore(db, "readwrite");
 | |
|     await Promise.all(rollouts.map(rollout => store.put(rollout)));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Test whether there is a rollout in storage with the given slug.
 | |
|    * @param {string} slug
 | |
|    * @returns {Promise<boolean>}
 | |
|    */
 | |
|   async has(slug) {
 | |
|     const db = await getDatabase();
 | |
|     const rollout = await getStore(db, "readonly").get(slug);
 | |
|     return !!rollout;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get a rollout by slug
 | |
|    * @param {string} slug
 | |
|    */
 | |
|   async get(slug) {
 | |
|     const db = await getDatabase();
 | |
|     return getStore(db, "readonly").get(slug);
 | |
|   },
 | |
| 
 | |
|   /** Get all rollouts in the database. */
 | |
|   async getAll() {
 | |
|     const db = await getDatabase();
 | |
|     return getStore(db, "readonly").getAll();
 | |
|   },
 | |
| 
 | |
|   /** Get all rollouts in the "active" state. */
 | |
|   async getAllActive() {
 | |
|     const rollouts = await this.getAll();
 | |
|     return rollouts.filter(rollout => rollout.state === this.STATE_ACTIVE);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Test wrapper that temporarily replaces the stored rollout data with fake
 | |
|    * data for testing.
 | |
|    */
 | |
|   withTestMock() {
 | |
|     return function(testFunction) {
 | |
|       return async function inner(...args) {
 | |
|         let db = await getDatabase();
 | |
|         const oldData = await getStore(db, "readonly").getAll();
 | |
|         await getStore(db, "readwrite").clear();
 | |
|         try {
 | |
|           await testFunction(...args);
 | |
|         } finally {
 | |
|           db = await getDatabase();
 | |
|           await getStore(db, "readwrite").clear();
 | |
|           const store = getStore(db, "readwrite");
 | |
|           await Promise.all(oldData.map(d => store.add(d)));
 | |
|         }
 | |
|       };
 | |
|     };
 | |
|   },
 | |
| };
 | 
