UNPKG

@itwin/core-backend

Version:
625 lines • 33.4 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudSqlite = void 0; const semver = require("semver"); const fs_1 = require("fs"); const path_1 = require("path"); const imodeljs_native_1 = require("@bentley/imodeljs-native"); const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const BlobContainerService_1 = require("./BlobContainerService"); const IModelHost_1 = require("./IModelHost"); const IModelJsFs_1 = require("./IModelJsFs"); const tracing_1 = require("./rpc/tracing"); // spell:ignore logmsg httpcode daemonless cachefile cacheslots ddthh cloudsqlite premajor preminor prepatch /** * Types for accessing SQLite databases stored in cloud containers. * @beta */ var CloudSqlite; (function (CloudSqlite) { const logInfo = (msg) => core_bentley_1.Logger.logInfo("CloudSqlite", msg); const logError = (msg) => core_bentley_1.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) core_common_1.CloudSqliteError.throwError("service-not-available", { message: `${serviceName} service is not available` }); return service; } function getBlobService() { return verifyService("BlobContainer", BlobContainerService_1.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_1.IModelHost.getAccessToken(); if (userToken === "") userToken = tracing_1.RpcTrace.currentActivity?.accessToken ?? ""; const response = await getBlobService().requestToken({ ...args, userToken }); return response?.token ?? ""; } CloudSqlite.requestToken = requestToken; function noLeadingOrTrailingSpaces(name, msg) { if (name.trim() !== name) core_common_1.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)) core_common_1.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 imodeljs_native_1.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 imodeljs_native_1.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 imodeljs_native_1.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 = core_bentley_1.BriefcaseStatus.DownloadCancelled; throw err; } finally { if (timer) clearInterval(timer); } } CloudSqlite.cleanDeletedBlocks = cleanDeletedBlocks; /** @internal */ async function transferDb(direction, container, props) { if (direction === "download") (0, fs_1.mkdirSync)((0, path_1.dirname)(props.localFileName), { recursive: true }); // make sure the directory exists before starting download let timer; try { const transfer = new imodeljs_native_1.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 = core_bentley_1.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) core_common_1.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 core_common_1.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) core_common_1.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 { } core_common_1.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) core_common_1.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) { core_common_1.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 ?? (0, path_1.join)(IModelHost_1.IModelHost.profileDir, "CloudCaches", cacheName); IModelJsFs_1.IModelJsFs.recursiveMkDirSync(rootDir); const cache = new imodeljs_native_1.NativeLibrary.nativeLib.CloudCache({ rootDir, name: cacheName, cacheSize: args.cacheSize ?? "10G" }); if (core_bentley_1.Logger.getLevel("CloudSqlite") === core_bentley_1.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_1.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 = (0, path_1.join)(IModelHost_1.KnownLocations.tmpdir, "blank.db"); args.dbType.createNewDb(localFileName, args); await transferDb("upload", container, { dbName: args.dbName, localFileName }); (0, fs_1.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_1.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, core_bentley_1.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 core_bentley_1.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 core_bentley_1.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") core_common_1.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: tracing_1.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 || (exports.CloudSqlite = CloudSqlite = {})); //# sourceMappingURL=CloudSqlite.js.map