910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const dbName = 'sweeper';
const storeName = 'items';
const indexName = 'version';

let db;

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function createDatabaseCallback(newDb) {
  db = newDb;
  const store = db.createObjectStore(storeName, {keyPath: 'id'});
  store.createIndex(indexName, indexName, {unique: false});
}

const putItems = (versionNum, numItems) => {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, 'readwrite');
    const objectStore = tx.objectStore(storeName);
    for (let i = 0; i < numItems; i++) {
      objectStore.put({id: i, version: versionNum});
    }
    tx.oncomplete = () => {
      console.log(`Put ${numItems} items at version ${versionNum}`);
      resolve();
    };
    tx.onerror = reject;
  });
};

const getIndexCount = async (versionNum = undefined) => {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(storeName, 'readonly');
    let request =
        transaction.objectStore(storeName).index(indexName).count(versionNum);

    request.onsuccess = event => {
      const count = event.target.result;
      console.log(`Index count for version ${versionNum}: ${count}`);
      resolve(count);  // resolve the promise with the int
    };
    request.onerror = reject;
    transaction.onerror = reject;
  });
};

// Updates the items individually, but aborts the transaction.
const updateAndAbortTransaction = (currentVersion, numItems, sweeperDelay) => {
  return new Promise((resolve, reject) => {
    const nextVersion = currentVersion + 1;
    const updateTxn = db.transaction(storeName, 'readwrite');
    const objectStore = updateTxn.objectStore(storeName);
    updateTxn.onabort = resolve;
    updateTxn.onerror = reject;
    const txnStartTime = Date.now();
    for (let i = 0; i < numItems; i++) {
      const request = objectStore.put({id: i, version: nextVersion});
      if (i == numItems - 1) {
        request.onsuccess = () => {
          // Wait for the sweeper to run before aborting.
          while (Date.now() - txnStartTime < sweeperDelay) {
          }
          updateTxn.abort();
        }
      }
    }
  });
};

// Regression test for crbug.com/413540372.
// The browser test creates an object store with indexes and generates
// tombstones through update operations and triggers the sweeper through a read
// call. While the tombstone sweeper is running, another transaction is started
// to update the indexes and then aborted. The sweeper, if still running along
// with this update call, deletes the tombstones generated by this update,
// but since the update transaction gets aborted, the index entries are lost.
async function runRollbackTest(numEntries, sweeperDelay) {
  return new Promise(async (resolve, reject) => {
    try {
      await promiseDeleteThenOpenDb(dbName, createDatabaseCallback);
      let currentVersion = 0;
      await putItems(currentVersion, numEntries);
      ++currentVersion;
      await putItems(currentVersion, numEntries);
      await getIndexCount();
      await delay(sweeperDelay);
      await updateAndAbortTransaction(currentVersion, numEntries, sweeperDelay);
      await delay(2 * sweeperDelay);
      let count = await getIndexCount(currentVersion);
      if (count !== numEntries) {
        reject(new Error(`Expected ${numEntries} entries, got ${count}`));
      }
      resolve();
    } catch (error) {
      console.error('Error during rollback test:', error);
      reject(error);
    }
  });
}

// Regression test for crbug.com/413540372.
// The browser test creates an object store with indexes and generates
// tombstones through update operations and triggers the sweeper through a read
// call. The tombstone sweeper completes one run and waits for any incoming
// calls. While the sweeper is paused, an update to the database is called with
// the same version. In the regression scenario, the sweeper still runs with an
// iterator based on an outdated snapshot of the database, it reads the stale
// index data and incorrectly deletes proper values as tombstones.
async function runInterleavedTest(
    numEntries, delayBeforeInterleavedUpdates, delayToFinishSweeper) {
  return new Promise(async (resolve, reject) => {
    try {
      await promiseDeleteThenOpenDb(dbName, createDatabaseCallback);
      let currentVersion = 0;
      await putItems(currentVersion, numEntries);
      ++currentVersion;
      await putItems(currentVersion, numEntries);
      await getIndexCount();

      // Allow time for the sweeper to start.
      await delay(delayBeforeInterleavedUpdates);

      // Call put() with the same version on 100 items.
      await putItems(currentVersion, 100);

      // Wait for the sweeper to finish.
      await delay(delayToFinishSweeper);

      let count = await getIndexCount(currentVersion);
      if (count !== numEntries) {
        reject(new Error(`Expected ${numEntries} entries, got ${count}`));
      }
      resolve();
    } catch (error) {
      console.error('Error during interleaved operations test:', error);
      reject(error);
    }
  });
}