@metamask/kernel-store
Version:
Ocap Kernel storage abstractions and implementations
295 lines • 9.89 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeSQLKernelDatabase = makeSQLKernelDatabase;
exports.getDBFilename = getDBFilename;
const logger_1 = require("@metamask/logger");
// eslint-disable-next-line @typescript-eslint/naming-convention
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const promises_1 = require("fs/promises");
const os_1 = require("os");
const path_1 = require("path");
const common_ts_1 = require("./common.cjs");
const env_ts_1 = require("./env.cjs");
/**
* Ensure that SQLite is initialized.
*
* @param dbFilename - The filename of the database to use.
* @param logger - The logger to use, if any.
* @returns The SQLite database object.
*/
async function initDB(dbFilename, logger) {
const dbPath = await getDBFilename(dbFilename);
logger?.debug('dbPath:', dbPath);
const db = new better_sqlite3_1.default(dbPath, {
verbose: (logger ? logger.info.bind(logger) : undefined),
});
db._spStack = [];
return db;
}
/**
* Makes a persistent {@link KVStore} on top of a SQLite database.
*
* @param db - The (open) database to use.
* @returns A key/value store using the given database.
*/
function makeKVStore(db) {
const sqlKVInit = db.prepare(common_ts_1.SQL_QUERIES.CREATE_TABLE);
sqlKVInit.run();
const sqlKVGet = db.prepare(common_ts_1.SQL_QUERIES.GET);
sqlKVGet.pluck(true);
/**
* Read a key's value from the database.
*
* @param key - A key to fetch.
* @param required - True if it is an error for the entry not to be there.
* @returns The value at that key.
*/
function kvGet(key, required) {
const result = sqlKVGet.get(key);
if (required && !result) {
throw Error(`no record matching key '${key}'`);
}
return result;
}
const sqlKVGetNextKey = db.prepare(common_ts_1.SQL_QUERIES.GET_NEXT);
sqlKVGetNextKey.pluck(true);
/**
* Get the lexicographically next key in the KV store after a given key.
*
* @param previousKey - The key you want to know the key after.
*
* @returns The key after `previousKey`, or undefined if `previousKey` is the
* last key in the store.
*/
function kvGetNextKey(previousKey) {
if (typeof previousKey !== 'string') {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`previousKey ${previousKey} must be a string`);
}
return sqlKVGetNextKey.get(previousKey);
}
const sqlKVSet = db.prepare(common_ts_1.SQL_QUERIES.SET);
/**
* Set the value associated with a key in the database.
*
* @param key - A key to assign.
* @param value - The value to assign to it.
*/
function kvSet(key, value) {
sqlKVSet.run(key, value);
}
const sqlKVDelete = db.prepare(common_ts_1.SQL_QUERIES.DELETE);
/**
* Delete a key from the database.
*
* @param key - The key to remove.
*/
function kvDelete(key) {
sqlKVDelete.run(key);
}
return {
get: (key) => kvGet(key, false),
getNextKey: kvGetNextKey,
getRequired: (key) => kvGet(key, true),
set: kvSet,
delete: kvDelete,
};
}
/**
* Makes a {@link KernelDatabase} for low-level persistent storage.
*
* @param options - The options for the database.
* @param options.dbFilename - The filename of the database to use. Defaults to {@link DEFAULT_DB_FILENAME}.
* @param options.logger - A logger to use.
* @returns The key/value store to base the kernel store on.
*/
async function makeSQLKernelDatabase({ dbFilename, logger, }) {
const db = await initDB(dbFilename ?? common_ts_1.DEFAULT_DB_FILENAME, logger);
const kvStore = makeKVStore(db);
const sqlKVInitVS = db.prepare(common_ts_1.SQL_QUERIES.CREATE_TABLE_VS);
sqlKVInitVS.run();
const sqlKVClear = db.prepare(common_ts_1.SQL_QUERIES.CLEAR);
const sqlKVClearVS = db.prepare(common_ts_1.SQL_QUERIES.CLEAR_VS);
const sqlVatstoreGetAll = db.prepare(common_ts_1.SQL_QUERIES.GET_ALL_VS);
const sqlVatstoreSet = db.prepare(common_ts_1.SQL_QUERIES.SET_VS);
const sqlVatstoreDelete = db.prepare(common_ts_1.SQL_QUERIES.DELETE_VS);
const sqlVatstoreDeleteAll = db.prepare(common_ts_1.SQL_QUERIES.DELETE_VS_ALL);
const sqlBeginTransaction = db.prepare(common_ts_1.SQL_QUERIES.BEGIN_TRANSACTION);
const sqlCommitTransaction = db.prepare(common_ts_1.SQL_QUERIES.COMMIT_TRANSACTION);
const sqlAbortTransaction = db.prepare(common_ts_1.SQL_QUERIES.ABORT_TRANSACTION);
/**
* Begin a transaction if not already in one
*
* @returns True if a new transaction was started, false if already in one
*/
function beginIfNeeded() {
if (db.inTransaction) {
return false;
}
sqlBeginTransaction.run();
return true;
}
/**
* Commit a transaction if one is active and no savepoints remain
*/
function commitIfNeeded() {
if (db.inTransaction && db._spStack.length === 0) {
sqlCommitTransaction.run();
}
}
/**
* Rollback a transaction
*/
function rollbackIfNeeded() {
if (db.inTransaction) {
sqlAbortTransaction.run();
db._spStack.length = 0;
}
}
/**
* Delete everything from the database.
*/
function kvClear() {
sqlKVClear.run();
sqlKVClearVS.run();
}
/**
* Execute an arbitrary query and return the results.
*
* @param sql - The query to execute.
* @returns The results
*/
function kvExecuteQuery(sql) {
const query = db.prepare(sql);
return query.all();
}
/**
* Create a new VatStore for a vat.
*
* @param vatID - The vat for which this is being done.
*
* @returns a a VatStore object for the given vat.
*/
function makeVatStore(vatID) {
/**
* Fetch all the data in the vatstore.
*
* @returns the vatstore contents as a key-value Map.
*/
function getKVData() {
const result = [];
for (const kvPair of sqlVatstoreGetAll.iterate(vatID)) {
const { key, value } = kvPair;
result.push([key, value]);
}
return result;
}
/**
* Update the state of the vatstore
*
* @param sets - A map of key values that have been changed.
* @param deletes - A set of keys that have been deleted.
*/
function updateKVData(sets, deletes) {
db.transaction(() => {
for (const [key, value] of sets) {
sqlVatstoreSet.run(vatID, key, value);
}
for (const value of deletes) {
sqlVatstoreDelete.run(vatID, value);
}
})();
}
return {
getKVData,
updateKVData,
};
}
/**
* Delete an entire VatStore.
*
* @param vatId - The vat whose store is to be deleted.
*/
function deleteVatStore(vatId) {
sqlVatstoreDeleteAll.run(vatId);
}
/**
* Create a savepoint in the database.
*
* @param name - The name of the savepoint.
*/
function createSavepoint(name) {
// We must be in a transaction when creating the savepoint or releasing it
// later will cause an autocommit.
// See https://github.com/Agoric/agoric-sdk/issues/8423
beginIfNeeded();
(0, common_ts_1.assertSafeIdentifier)(name);
const query = common_ts_1.SQL_QUERIES.CREATE_SAVEPOINT.replace('%NAME%', name);
db.exec(query);
db._spStack.push(name);
}
/**
* Rollback to a savepoint in the database.
*
* @param name - The name of the savepoint.
*/
function rollbackSavepoint(name) {
(0, common_ts_1.assertSafeIdentifier)(name);
const idx = db._spStack.lastIndexOf(name);
if (idx < 0) {
throw new Error(`No such savepoint: ${name}`);
}
const query = common_ts_1.SQL_QUERIES.ROLLBACK_SAVEPOINT.replace('%NAME%', name);
db.exec(query);
db._spStack.splice(idx);
if (db._spStack.length === 0) {
rollbackIfNeeded();
}
}
/**
* Release a savepoint in the database.
*
* @param name - The name of the savepoint.
*/
function releaseSavepoint(name) {
(0, common_ts_1.assertSafeIdentifier)(name);
const idx = db._spStack.lastIndexOf(name);
if (idx < 0) {
throw new Error(`No such savepoint: ${name}`);
}
const query = common_ts_1.SQL_QUERIES.RELEASE_SAVEPOINT.replace('%NAME%', name);
db.exec(query);
db._spStack.splice(idx);
if (db._spStack.length === 0) {
commitIfNeeded();
}
}
return {
kernelKVStore: kvStore,
executeQuery: kvExecuteQuery,
clear: db.transaction(kvClear),
makeVatStore,
deleteVatStore,
createSavepoint,
rollbackSavepoint,
releaseSavepoint,
};
}
/**
* Get the filename for a database.
*
* @param label - A label for the database.
* @returns The filename for the database.
*/
async function getDBFilename(label) {
if (label.startsWith(':')) {
return label;
}
const dbRoot = (0, path_1.join)((0, os_1.tmpdir)(), './ocap-sqlite', (0, env_ts_1.getDBFolder)());
await (0, promises_1.mkdir)(dbRoot, { recursive: true });
return (0, path_1.join)(dbRoot, label);
}
//# sourceMappingURL=nodejs.cjs.map