/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module SQLiteDb
 */
import * as semver from "semver";
import { mkdirSync, unlinkSync } from "fs";
import { dirname, join } from "path";
import { NativeLibrary } from "@bentley/imodeljs-native";
import { BeDuration, BriefcaseStatus, Logger, LogLevel, OpenMode, StopWatch, } from "@itwin/core-bentley";
import { CloudSqliteError } from "@itwin/core-common";
import { BlobContainer } from "./BlobContainerService";
import { IModelHost, KnownLocations } from "./IModelHost";
import { IModelJsFs } from "./IModelJsFs";
import { RpcTrace } from "./rpc/tracing";
// spell:ignore logmsg httpcode daemonless cachefile cacheslots ddthh cloudsqlite premajor preminor prepatch
/**
 * Types for accessing SQLite databases stored in cloud containers.
 * @beta
 */
export var CloudSqlite;
(function (CloudSqlite) {
    const logInfo = (msg) => Logger.logInfo("CloudSqlite", msg);
    const logError = (msg) => Logger.logError("CloudSqlite", msg);
    /** Add (or replace) a property to an object that is not enumerable.
     * This is important so this member will be skipped when the object is the target of
     * [structuredClone](https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
     * (e.g. when the object is part of an exception that is marshalled across process boundaries.)
     */
    function addHiddenProperty(o, p, value) {
        return Object.defineProperty(o, p, { enumerable: false, writable: true, value });
    }
    CloudSqlite.addHiddenProperty = addHiddenProperty;
    function verifyService(serviceName, service) {
        if (undefined === service)
            CloudSqliteError.throwError("service-not-available", { message: `${serviceName} service is not available` });
        return service;
    }
    function getBlobService() {
        return verifyService("BlobContainer", BlobContainer.service);
    }
    CloudSqlite.getBlobService = getBlobService;
    /**
     * Request a new AccessToken for a cloud container using the [[BlobContainer]] service.
     * If the service is unavailable or returns an error, an empty token is returned.
     */
    async function requestToken(args) {
        // allow the userToken to be supplied via args. If not supplied, or blank, use the backend's accessToken. If that fails, use the value from the current RPC request
        let userToken = args.userToken ? args.userToken : await IModelHost.getAccessToken();
        if (userToken === "")
            userToken = RpcTrace.currentActivity?.accessToken ?? "";
        const response = await getBlobService().requestToken({ ...args, userToken });
        return response?.token ?? "";
    }
    CloudSqlite.requestToken = requestToken;
    function noLeadingOrTrailingSpaces(name, msg) {
        if (name.trim() !== name)
            CloudSqliteError.throwError("invalid-name", { message: `${msg} [${name}] may not have leading or trailing spaces` });
    }
    CloudSqlite.noLeadingOrTrailingSpaces = noLeadingOrTrailingSpaces;
    function validateDbName(dbName) {
        if (dbName === "" || dbName.length > 255 || /[#\.<>:"/\\"`'|?*\u0000-\u001F]/g.test(dbName) || /^(con|prn|aux|nul|com\d|lpt\d)$/i.test(dbName))
            CloudSqliteError.throwError("invalid-name", { message: "invalid dbName", dbName });
        noLeadingOrTrailingSpaces(dbName, "dbName");
    }
    CloudSqlite.validateDbName = validateDbName;
    /**
     * Create a new CloudContainer from a ContainerAccessProps. For non-public containers, a valid accessToken must be provided before the container
     * can be used (e.g. via [[CloudSqlite.requestToken]]).
     * @note After the container is successfully connected to a CloudCache, it will begin auto-refreshing its accessToken every `tokenRefreshSeconds` seconds (default is 1 hour)
     * until it is disconnected. However, if the container is public, or if `tokenRefreshSeconds` is <=0, auto-refresh is not enabled.
     */
    function createCloudContainer(args) {
        const container = new NativeLibrary.nativeLib.CloudContainer(args);
        // we're going to add these fields to the newly created object. They should *not* be enumerable so they are not copied
        // when the object is cloned (e.g. when included in an exception across processes).
        addHiddenProperty(container, "timer");
        addHiddenProperty(container, "refreshPromise");
        const refreshSeconds = (undefined !== args.tokenRefreshSeconds) ? args.tokenRefreshSeconds : 60 * 60; // default is 1 hour
        container.lockExpireSeconds = args.lockExpireSeconds ?? 60 * 60; // default is 1 hour
        // don't refresh tokens for public containers or if refreshSeconds<=0
        if (!args.isPublic && refreshSeconds > 0) {
            const tokenProps = { baseUri: args.baseUri, containerId: args.containerId, accessLevel: args.accessLevel };
            const doRefresh = async () => {
                let newToken;
                const url = `[${tokenProps.baseUri}/${tokenProps.containerId}]`;
                try {
                    newToken = await (args.tokenFn ?? CloudSqlite.requestToken)(tokenProps);
                    logInfo(`Refreshed token for container ${url}`);
                }
                catch (err) {
                    logError(`Error refreshing token for container ${url}: ${err.message}`);
                }
                container.accessToken = newToken ?? "";
            };
            const tokenRefreshFn = () => {
                container.timer = setTimeout(async () => {
                    container.refreshPromise = doRefresh(); // this promise is stored on the container so it can be awaited in tests
                    await container.refreshPromise;
                    container.refreshPromise = undefined;
                    tokenRefreshFn(); // schedule next refresh
                }, refreshSeconds * 1000).unref(); // unref so it doesn't keep the process alive
            };
            addHiddenProperty(container, "onConnected", tokenRefreshFn); // schedule the first refresh when the container is connected
            addHiddenProperty(container, "onDisconnect", () => {
                if (container.timer !== undefined) {
                    clearTimeout(container.timer);
                    container.timer = undefined;
                }
            });
        }
        return container;
    }
    CloudSqlite.createCloudContainer = createCloudContainer;
    /** Begin prefetching all blocks for a database in a CloudContainer in the background. */
    function startCloudPrefetch(container, dbName, args) {
        return new NativeLibrary.nativeLib.CloudPrefetch(container, dbName, args);
    }
    CloudSqlite.startCloudPrefetch = startCloudPrefetch;
    ;
    /** Logging categories for `CloudCache.setLogMask` */
    let LoggingMask;
    (function (LoggingMask) {
        /** log all HTTP requests and responses */
        LoggingMask[LoggingMask["HTTP"] = 1] = "HTTP";
        /** log as blocks become dirty and must be uploaded */
        LoggingMask[LoggingMask["DirtyBlocks"] = 2] = "DirtyBlocks";
        /** log as blocks are added to the delete list */
        LoggingMask[LoggingMask["AddToDelete"] = 4] = "AddToDelete";
        /** log container lifecycle events (e.g. authorization requests, disconnects, and state transitions) */
        LoggingMask[LoggingMask["LifecycleEvents"] = 8] = "LifecycleEvents";
        /** Turn on all logging categories */
        LoggingMask[LoggingMask["All"] = 255] = "All";
        /** Disable logging */
        LoggingMask[LoggingMask["None"] = 0] = "None";
    })(LoggingMask = CloudSqlite.LoggingMask || (CloudSqlite.LoggingMask = {}));
    /**
     * Clean any unused deleted blocks from cloud storage. Unused deleted blocks can accumulate in cloud storage in a couple of ways:
     * 1) When a database is updated, a subset of its blocks are replaced by new versions, sometimes leaving the originals unused.
     * 2) A database is deleted with [[CloudContainer.deleteDatabase]]
     * In both cases, the blocks are not deleted immediately. Instead, they are scheduled for deletion at some later time.
     * Calling this method deletes all blocks in the cloud container for which the scheduled deletion time has passed.
     * @param container the CloudContainer to be cleaned. Must be connected and hold the write lock.
     * @param options options for the cleanup operation. @see CloudSqlite.CleanDeletedBlocksOptions
     */
    async function cleanDeletedBlocks(container, options) {
        let timer;
        try {
            const cleanJob = new NativeLibrary.nativeLib.CancellableCloudSqliteJob("cleanup", container, options);
            let total = 0;
            const onProgress = options?.onProgress;
            if (onProgress) {
                timer = setInterval(async () => {
                    const progress = cleanJob.getProgress();
                    total = progress.total;
                    const result = await onProgress(progress.loaded, progress.total);
                    if (result === 1)
                        cleanJob.stopAndSaveProgress();
                    else if (result !== 0)
                        cleanJob.cancelTransfer();
                }, 250);
            }
            await cleanJob.promise;
            await onProgress?.(total, total); // make sure we call progress func one last time when download completes
            container.checkForChanges(); // re-read the manifest so the number of garbage blocks is updated.
        }
        catch (err) {
            if (err.message === "cancelled")
                err.errorNumber = BriefcaseStatus.DownloadCancelled;
            throw err;
        }
        finally {
            if (timer)
                clearInterval(timer);
        }
    }
    CloudSqlite.cleanDeletedBlocks = cleanDeletedBlocks;
    /** @internal */
    async function transferDb(direction, container, props) {
        if (direction === "download")
            mkdirSync(dirname(props.localFileName), { recursive: true }); // make sure the directory exists before starting download
        let timer;
        try {
            const transfer = new NativeLibrary.nativeLib.CancellableCloudSqliteJob(direction, container, props);
            let total = 0;
            const onProgress = props.onProgress;
            if (onProgress) {
                timer = setInterval(async () => {
                    const progress = transfer.getProgress();
                    total = progress.total;
                    if (onProgress(progress.loaded, progress.total))
                        transfer.cancelTransfer();
                }, 250);
            }
            await transfer.promise;
            onProgress?.(total, total); // make sure we call progress func one last time when download completes
        }
        catch (err) {
            if (err.message === "cancelled")
                err.errorNumber = BriefcaseStatus.DownloadCancelled;
            throw err;
        }
        finally {
            if (timer)
                clearInterval(timer);
        }
    }
    CloudSqlite.transferDb = transferDb;
    /** Upload a local SQLite database file into a CloudContainer.
     * @param container the CloudContainer holding the database. Must be connected.
     * @param props the properties that describe the database to be downloaded, plus optionally an `onProgress` function.
     * @note this function requires that the write lock be held on the container
     */
    async function uploadDb(container, props) {
        await transferDb("upload", container, props);
        container.checkForChanges(); // re-read the manifest so the database is available locally.
    }
    CloudSqlite.uploadDb = uploadDb;
    /** Download a database from a CloudContainer.
     * @param container the CloudContainer holding the database. Must be connected.
     * @param props the properties that describe the database to be downloaded, plus optionally an `onProgress` function.
     * @returns a Promise that is resolved when the download completes.
     * @note the download is "restartable." If the transfer is aborted and then re-requested, it will continue from where
     * it left off rather than re-downloading the entire file.
     */
    async function downloadDb(container, props) {
        await transferDb("download", container, props);
    }
    CloudSqlite.downloadDb = downloadDb;
    /**
      * Attempt to acquire the write lock for a container, with retries.
      * If write lock is held by another user, call busyHandler if supplied. If no busyHandler, or handler returns "stop", throw. Otherwise try again.
      * @note if write lock is already held by the same user, this function will refresh the write lock's expiry time.
      * @param user the name to be displayed to other users in the event they attempt to obtain the lock while it is held by us
      * @param container the CloudContainer for which the lock is to be acquired
      * @param busyHandler if present, function called when the write lock is currently held by another user.
      * @throws if [[container]] is not connected to a CloudCache.
      */
    async function acquireWriteLock(args) {
        const container = args.container;
        while (true) {
            try {
                // if the write is already held:
                // - by the same user, just update the write lock expiry (by calling acquireWriteLock).
                // - by another user, throw an error
                if (container.hasWriteLock && container.writeLockHeldBy !== args.user)
                    CloudSqliteError.throwError("write-lock-held", {
                        message: "lock in use", errorNumber: 5,
                        lockedBy: container.writeLockHeldBy ?? "",
                        expires: container.writeLockExpires
                    });
                container.acquireWriteLock(args.user);
                container.writeLockHeldBy = args.user;
                return;
            }
            catch (e) {
                if (e.errorNumber === 5 && args.busyHandler && "stop" !== await args.busyHandler(e.lockedBy, e.expires)) // 5 === BE_SQLITE_BUSY
                    continue; // busy handler wants to try again
                CloudSqliteError.throwError("write-lock-held", { message: e.message, ...e });
            }
        }
    }
    CloudSqlite.acquireWriteLock = acquireWriteLock;
    function getWriteLockHeldBy(container) {
        return container.writeLockHeldBy;
    }
    CloudSqlite.getWriteLockHeldBy = getWriteLockHeldBy;
    /** release the write lock on a container. */
    function releaseWriteLock(container) {
        container.releaseWriteLock();
        container.writeLockHeldBy = undefined;
    }
    CloudSqlite.releaseWriteLock = releaseWriteLock;
    /**
    * Perform an asynchronous write operation on a CloudContainer with the write lock held.
    * 1. if write lock is already held by the current user, refresh write lock's expiry time, call operation and return.
    * 2. attempt to acquire the write lock, with retries. Throw if unable to obtain write lock.
    * 3. perform the operation
    * 3.a if the operation throws, abandon all changes and re-throw
    * 4. release the write lock.
    * 5. return value from operation
    * @param user the name to be displayed to other users in the event they attempt to obtain the lock while it is held by us
    * @param container the CloudContainer for which the lock is to be acquired
    * @param operation an asynchronous operation performed with the write lock held.
    * @param busyHandler if present, function called when the write lock is currently held by another user.
    * @returns a Promise with the result of `operation`
    */
    async function withWriteLock(args, operation) {
        const containerInternal = args.container;
        const wasLockedBy = containerInternal.writeLockHeldBy;
        await acquireWriteLock(args);
        try {
            if (wasLockedBy === args.user) // If the user already had the write lock, then don't release it.
                return await operation();
            const val = await operation(); // wait for work to finish or fail
            releaseWriteLock(containerInternal);
            return val;
        }
        catch (e) {
            args.container.abandonChanges(); // if operation threw, abandon all changes
            containerInternal.writeLockHeldBy = undefined;
            throw e;
        }
    }
    CloudSqlite.withWriteLock = withWriteLock;
    /**
     * Parse the name of a Db stored in a CloudContainer into the dbName and version number. A single CloudContainer may hold
     * many versions of the same Db. The name of the Db in the CloudContainer is in the format "name:version". This
     * function splits them into separate strings.
     */
    function parseDbFileName(dbFileName) {
        const parts = dbFileName.split(":");
        return { dbName: parts[0], version: parts[1] ?? "" };
    }
    CloudSqlite.parseDbFileName = parseDbFileName;
    function validateDbVersion(version) {
        version = version ?? "0.0.0";
        const opts = { loose: true, includePrerelease: true };
        // clean allows prerelease, so try it first. If that fails attempt to coerce it (coerce strips prerelease even if you say not to.)
        const semVersion = semver.clean(version, opts) ?? semver.coerce(version, opts)?.version;
        if (!semVersion)
            CloudSqliteError.throwError("invalid-name", { message: "invalid version specification" });
        version = semVersion;
        return version;
    }
    CloudSqlite.validateDbVersion = validateDbVersion;
    function isSemverPrerelease(version) {
        return semver.major(version) === 0 || semver.prerelease(version);
    }
    CloudSqlite.isSemverPrerelease = isSemverPrerelease;
    function isSemverEditable(dbFullName, container) {
        return isSemverPrerelease(parseDbFileName(dbFullName).version) || container.queryDatabase(dbFullName)?.state === "copied";
    }
    CloudSqlite.isSemverEditable = isSemverEditable;
    /** Create a dbName for a database from its base name and version. This will be in the format "name:version" */
    function makeSemverName(dbName, version) {
        return `${dbName}:${validateDbVersion(version)}`;
    }
    CloudSqlite.makeSemverName = makeSemverName;
    /** query the databases in the supplied container for the highest SemVer match according to the version range. Throws if no version available for the range. */
    function querySemverMatch(props) {
        const dbName = props.dbName;
        const dbs = props.container.queryDatabases(`${dbName}*`); // get all databases that start with dbName
        const versions = [];
        for (const db of dbs) {
            const thisDb = parseDbFileName(db);
            if (thisDb.dbName === dbName && "string" === typeof thisDb.version && thisDb.version.length > 0)
                versions.push(thisDb.version);
        }
        if (versions.length === 0)
            versions[0] = "0.0.0";
        const range = props.version ?? "*";
        try {
            const version = semver.maxSatisfying(versions, range, { loose: true, includePrerelease: props.includePrerelease });
            if (version)
                return `${dbName}:${version}`;
        }
        catch { }
        CloudSqliteError.throwError("no-version-available", { message: `No version of '${dbName}' available for "${range}"`, ...props });
    }
    CloudSqlite.querySemverMatch = querySemverMatch;
    async function createNewDbVersion(container, args) {
        const oldFullName = CloudSqlite.querySemverMatch({ container, ...args.fromDb });
        const oldDb = CloudSqlite.parseDbFileName(oldFullName);
        const newVersion = semver.inc(oldDb.version, args.versionType, args.identifier);
        if (!newVersion)
            CloudSqliteError.throwError("invalid-name", { message: `cannot create new version for ${oldFullName}`, dbName: oldFullName, ...args });
        const newName = makeSemverName(oldDb.dbName, newVersion);
        try {
            await container.copyDatabase(oldFullName, newName);
        }
        catch (e) {
            CloudSqliteError.throwError("copy-error", { message: `Error attempting to create new version ${newName} from ${oldFullName}`, ...args, cause: e });
        }
        // return the old and new db names and versions
        return { oldDb, newDb: { dbName: oldDb.dbName, version: newVersion } };
    }
    CloudSqlite.createNewDbVersion = createNewDbVersion;
    /** The collection of currently extant `CloudCache`s, by name. */
    class CloudCaches {
        static cloudCaches = new Map();
        /** create a new CloudCache */
        static makeCache(args) {
            const cacheName = args.cacheName;
            const rootDir = args.cacheDir ?? join(IModelHost.profileDir, "CloudCaches", cacheName);
            IModelJsFs.recursiveMkDirSync(rootDir);
            const cache = new NativeLibrary.nativeLib.CloudCache({ rootDir, name: cacheName, cacheSize: args.cacheSize ?? "10G" });
            if (Logger.getLevel("CloudSqlite") === LogLevel.Trace) {
                cache.setLogMask(CloudSqlite.LoggingMask.All);
            }
            this.cloudCaches.set(cacheName, cache);
            return cache;
        }
        /** find a CloudCache by name, if it exists */
        static findCache(cacheName) {
            return this.cloudCaches.get(cacheName);
        }
        /** @internal */
        static dropCache(cacheName) {
            const cache = this.cloudCaches.get(cacheName);
            this.cloudCaches.delete(cacheName);
            return cache;
        }
        /** called by IModelHost after shutdown.
         * @internal
         */
        static destroy() {
            this.cloudCaches.forEach((cache) => cache.destroy());
            this.cloudCaches.clear();
        }
        /** Get a CloudCache by name. If the CloudCache doesn't yet exist, it is created. */
        static getCache(args) {
            return this.cloudCaches.get(args.cacheName) ?? this.makeCache(args);
        }
    }
    CloudSqlite.CloudCaches = CloudCaches;
    /** Class that provides convenient local access to a SQLite database in a CloudContainer.  */
    class DbAccess {
        /** The name of the database within the cloud container. */
        dbName;
        /** Parameters for obtaining the write lock for this container.  */
        lockParams = {
            user: "",
            nRetries: 20,
            retryDelayMs: 100,
        };
        static _cacheName = "default-64k";
        _container;
        _cloudDb;
        _writeLockProxy;
        _readerProxy;
        get _ctor() { return this.constructor; }
        /** @internal */
        static getCacheForClass() {
            return CloudCaches.getCache({ cacheName: this._cacheName });
        }
        _cache;
        /** only for tests
         * @internal
         */
        setCache(cache) {
            this._cache = cache;
        }
        /** @internal */
        getCache() {
            return this._cache ??= this._ctor.getCacheForClass();
        }
        /** @internal */
        getCloudDb() {
            return this._cloudDb;
        }
        /**
         * The token that grants access to the cloud container for this DbAccess. If it does not grant write permissions, all
         * write operations will fail. It should be refreshed (via a timer) before it expires.
         */
        get sasToken() { return this._container.accessToken; }
        set sasToken(token) { this._container.accessToken = token; }
        /** the container for this DbAccess. It is automatically connected to the CloudCache whenever it is accessed. */
        get container() {
            const container = this._container;
            if (!container.isConnected)
                container.connect(this.getCache());
            return container;
        }
        /** Start a prefetch operation to download all the blocks for the VersionedSqliteDb */
        startPrefetch() {
            return startCloudPrefetch(this.container, this.dbName);
        }
        /** Create a new DbAccess for a database stored in a cloud container. */
        constructor(args) {
            this._container = createCloudContainer({ writeable: true, ...args.props });
            this._cloudDb = new args.dbType(args.props);
            this.dbName = args.dbName;
            this.lockParams.user = IModelHost.userMoniker;
        }
        /** Close the database for this DbAccess, if it is open */
        closeDb() {
            if (this._cloudDb.isOpen)
                this._cloudDb.closeDb();
        }
        /** Close the database for this DbAccess if it is opened, and disconnect this `DbAccess from its CloudContainer. */
        close() {
            this.closeDb();
            this._container.disconnect();
        }
        /**
         * Initialize a cloud container to hold VersionedSqliteDbs. The container must first be created by [[createBlobContainer]].
         * This function creates and uploads an empty database into the container.
         * @note this deletes any existing content in the container.
         */
        static async _initializeDb(args) {
            const container = createCloudContainer({ ...args.props, writeable: true, accessToken: args.props.accessToken ?? await CloudSqlite.requestToken(args.props) });
            container.initializeContainer({ blockSize: args.blockSize === "4M" ? 4 * 1024 * 1024 : 64 * 1024 });
            container.connect(CloudCaches.getCache({ cacheName: this._cacheName }));
            await withWriteLock({ user: "initialize", container }, async () => {
                const localFileName = join(KnownLocations.tmpdir, "blank.db");
                args.dbType.createNewDb(localFileName, args);
                await transferDb("upload", container, { dbName: args.dbName, localFileName });
                unlinkSync(localFileName);
            });
            container.disconnect({ detach: true });
        }
        /**
         * Create a new BlobContainer from the BlobContainer service to hold one or more VersionedSqliteDbs.
         * @returns A ContainerProps that describes the newly created container.
         * @note the current user must have administrator rights to create containers.
         */
        static async createBlobContainer(args) {
            const auth = verifyService("Authorization Client", IModelHost.authorizationClient);
            const userToken = await auth.getAccessToken();
            const cloudContainer = await getBlobService().create({ scope: args.scope, metadata: args.metadata, userToken });
            return { baseUri: cloudContainer.baseUri, containerId: cloudContainer.containerId, storageType: cloudContainer.provider };
        }
        /**
         * Synchronize the local cache of this database with any changes by made by others.
         * @note This is called automatically whenever any write operation is performed on this DbAccess. It is only necessary to
         * call this directly if you have not changed the database recently, but wish to perform a readonly operation and want to
         * ensure it is up-to-date as of now.
         * @note There is no guarantee that the database is up-to-date even immediately after calling this method, since others
         * may be modifying it at any time.
         */
        synchronizeWithCloud() {
            this.closeDb();
            this.container.checkForChanges();
        }
        /**
         * Ensure that the database controlled by this `DbAccess` is open for read access and return the database object.
         * @note if the database is already open (either for read or write), this method merely returns the database object.
         */
        openForRead() {
            if (!this._cloudDb.isOpen)
                this._cloudDb.openDb(this.dbName, OpenMode.Readonly, this.container);
            return this._cloudDb;
        }
        /**
         * Perform an operation on this database with the lock held and the database opened for write
         * @param operationName the name of the operation. Only used for logging.
         * @param operation a function called with the lock held and the database open for write.
         * @returns A promise that resolves to the the return value of `operation`.
         * @see `SQLiteDb.withLockedContainer`
         * @note Most uses of `CloudSqliteDbAccess` require that the lock not be held by any operation for long. Make sure you don't
         * do any avoidable or time consuming work in your operation function.
         */
        async withLockedDb(args, operation) {
            let nRetries = this.lockParams.nRetries;
            const cacheGuid = this.container.cache.guid; // eslint-disable-line @typescript-eslint/no-non-null-assertion
            const user = args.user ?? this.lockParams.user ?? cacheGuid;
            const timer = new StopWatch(undefined, true);
            const showMs = () => `(${timer.elapsed.milliseconds}ms)`;
            const busyHandler = async (lockedBy, expires) => {
                if (--nRetries <= 0) {
                    if ("stop" === await this.lockParams.onFailure?.(lockedBy, expires))
                        return "stop";
                    nRetries = this.lockParams.nRetries;
                }
                const delay = this.lockParams.retryDelayMs;
                logInfo(`lock retry for ${cacheGuid} after ${showMs()}, waiting ${delay}`);
                await BeDuration.fromMilliseconds(delay).wait();
            };
            this.closeDb(); // in case it is currently open for read.
            let lockObtained = false;
            const operationName = args.operationName;
            try {
                return await this._cloudDb.withLockedContainer({ user, dbName: this.dbName, container: this.container, busyHandler, openMode: args.openMode }, async () => {
                    lockObtained = true;
                    logInfo(`lock acquired by ${cacheGuid} for ${operationName} ${showMs()}`);
                    return operation();
                });
            }
            finally {
                if (lockObtained)
                    logInfo(`lock released by ${cacheGuid} after ${operationName} ${showMs()} `);
                else
                    logError(`could not obtain lock for ${cacheGuid} to perform ${operationName} ${showMs()} `);
            }
        }
        /** get a method member, by name, from the database object. Throws if not a Function. */
        getDbMethod(methodName) {
            const fn = this._cloudDb[methodName];
            if (typeof fn !== "function")
                CloudSqliteError.throwError("not-a-function", { message: `illegal method name ${methodName}`, dbName: this.dbName });
            return fn;
        }
        /**
         * A Proxy Object to call a writeable async method on the cloud database controlled by this `DbAccess`.
         *
         * Whenever a method is called through this Proxy, it will:
         * - attempt to acquire the write lock on the container
         * - open the database for write
         * - call the method
         * - close the database
         * - upload changes
         * - release the write lock.
         *
         * @see [[withLockedDb]]
         */
        get writeLocker() {
            return this._writeLockProxy ??= new Proxy(this, {
                get(access, operationName) {
                    const fn = access.getDbMethod(operationName);
                    return async (...args) => access.withLockedDb({ operationName, user: RpcTrace.currentActivity?.user }, fn.bind(access._cloudDb, ...args));
                },
            });
        }
        /**
         * A Proxy Object to call a synchronous readonly method on the database controlled by this `DbAccess`.
         * Whenever a method is called through this Proxy, it will first ensure that the database is opened for at least read access.
         */
        get reader() {
            return this._readerProxy ??= new Proxy(this, {
                get(access, methodName) {
                    const fn = access.getDbMethod(methodName);
                    return (...args) => fn.call(access.openForRead(), ...args);
                },
            });
        }
    }
    CloudSqlite.DbAccess = DbAccess;
})(CloudSqlite || (CloudSqlite = {}));
//# sourceMappingURL=CloudSqlite.js.map