Repository: servo/servo
File: components/storage/indexeddb/mod.rs (Rust)

Code before:
            result
                .map_err(|err| format!("{err:?}"))
                .map_err(BackendError::from),
        );
    }

    fn version(&self) -> Result<u64, E::Error> {
        self.engine.version()
    }

    fn set_version(&mut self, version: u64) -> DbResult<()> {
        self.engine
            .set_version(version)
            .map_err(|err| format!("{err:?}"))
    }
}

fn backend_error_from_sqlite_error(err: RusqliteError) -> BackendError {
    if is_sqlite_disk_full_error(&err) {
        BackendError::QuotaExceeded
    } else {
        BackendError::DbErr(format!("{err:?}"))
    }
}

fn is_sqlite_disk_full_error(error: &RusqliteError) -> bool {
    fn has_enospc(mut source: Option<&(dyn StdError + 'static)>) -> bool {
        while let Some(err) = source {
            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
                if io_err.raw_os_error() == Some(ENOSPC) {
                    return true;
                }
            }
            source = err.source();
        }
        false
    }

    // Walk the full chain (including `error` itself).
    let saw_enospc = has_enospc(Some(error as &(dyn StdError + 'static)));

    match error {
        RusqliteError::SqliteFailure(sqlite_err, _) => {
            // High confidence "database or disk is full".
            if sqlite_err.code == ffi::ErrorCode::DiskFull ||
                sqlite_err.extended_code == ffi::SQLITE_FULL
            {
                return true;
            }

            // Only treat IO errors as quota-related if ENOSPC is present.

Review comment (suggestion):
Seems to me like this could be useful for other parts of storage, so maybe put it in `https://github.com/servo/servo/blob/main/components/storage/shared/mod.rs or elsewhere in `storage`? cc @janvarga 

Code after:
                .map_err(|err| format!("{err:?}"))
                .map_err(BackendError::from),
        );
    }

    fn version(&self) -> Result<u64, E::Error> {
        self.engine.version()
    }

    fn set_version(&mut self, version: u64) -> DbResult<()> {
        self.engine
            .set_version(version)
            .map_err(|err| format!("{err:?}"))
    }
}

fn backend_error_from_sqlite_error(err: RusqliteError) -> BackendError {
    if is_sqlite_disk_full_error(&err) {
        BackendError::QuotaExceeded
    } else {
        BackendError::DbErr(format!("{err:?}"))
    }
}

/// Keeping track of pending upgrade transactions.
/// TODO: move into general transaction lifecycle.
struct PendingUpgrade {
    sender: GenericCallback<BackendResult<OpenDatabaseResult>>,
    db_version: u64,
}

struct IndexedDBManager {
    port: GenericReceiver<IndexedDBThreadMsg>,
    idb_base_dir: PathBuf,
    databases: HashMap<IndexedDBDescription, IndexedDBEnvironment<SqliteEngine>>,
    thread_pool: Arc<ThreadPool>,
    /// Tracking pending upgrade transactions.
    /// TODO: move into general transaction lifecyle.
    pending_upgrades: HashMap<(String, u64), PendingUpgrade>,
    /// A global counter to produce unique transaction ids.
    /// TODO: remove once db connections lifecyle is managed.
    /// A global counter is only necessary because of how deleting a db currently
    /// does not wait for connection to close and transactions to finish.
    serial_number_counter: u64,
}

impl IndexedDBManager {
    fn new(port: GenericReceiver<IndexedDBThreadMsg>, idb_base_dir: PathBuf) -> IndexedDBManager {
        debug!("New indexedDBManager");

        // Uses an estimate of the system cpus to process IndexedDB transactions