UNPKG

@metamask/kernel-store

Version:
295 lines 9.4 kB
function $importDefault(module) { if (module?.__esModule) { return module.default; } return module; } import { Logger } from "@metamask/logger"; // eslint-disable-next-line @typescript-eslint/naming-convention import $Sqlite from "better-sqlite3"; const Sqlite = $importDefault($Sqlite); import { mkdir } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { SQL_QUERIES, DEFAULT_DB_FILENAME, assertSafeIdentifier } from "./common.mjs"; import { getDBFolder } from "./env.mjs"; /** * 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 Sqlite(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(SQL_QUERIES.CREATE_TABLE); sqlKVInit.run(); const sqlKVGet = db.prepare(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(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(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(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. */ export async function makeSQLKernelDatabase({ dbFilename, logger, }) { const db = await initDB(dbFilename ?? DEFAULT_DB_FILENAME, logger); const kvStore = makeKVStore(db); const sqlKVInitVS = db.prepare(SQL_QUERIES.CREATE_TABLE_VS); sqlKVInitVS.run(); const sqlKVClear = db.prepare(SQL_QUERIES.CLEAR); const sqlKVClearVS = db.prepare(SQL_QUERIES.CLEAR_VS); const sqlVatstoreGetAll = db.prepare(SQL_QUERIES.GET_ALL_VS); const sqlVatstoreSet = db.prepare(SQL_QUERIES.SET_VS); const sqlVatstoreDelete = db.prepare(SQL_QUERIES.DELETE_VS); const sqlVatstoreDeleteAll = db.prepare(SQL_QUERIES.DELETE_VS_ALL); const sqlBeginTransaction = db.prepare(SQL_QUERIES.BEGIN_TRANSACTION); const sqlCommitTransaction = db.prepare(SQL_QUERIES.COMMIT_TRANSACTION); const sqlAbortTransaction = db.prepare(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(); assertSafeIdentifier(name); const query = 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) { assertSafeIdentifier(name); const idx = db._spStack.lastIndexOf(name); if (idx < 0) { throw new Error(`No such savepoint: ${name}`); } const query = 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) { assertSafeIdentifier(name); const idx = db._spStack.lastIndexOf(name); if (idx < 0) { throw new Error(`No such savepoint: ${name}`); } const query = 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. */ export async function getDBFilename(label) { if (label.startsWith(':')) { return label; } const dbRoot = join(tmpdir(), './ocap-sqlite', getDBFolder()); await mkdir(dbRoot, { recursive: true }); return join(dbRoot, label); } //# sourceMappingURL=nodejs.mjs.map