urned 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); } export 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", } ); } }, /** * 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} 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} */ 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))); } }; }; }, }; PK