@itwin/core-backend
Version:
iTwin.js backend components
625 lines • 33.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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
;