UNPKG

@metamask/kernel-store

Version:
364 lines 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initDB = initDB; exports.makeSQLKernelDatabase = makeSQLKernelDatabase; const logger_1 = require("@metamask/logger"); const sqlite_wasm_1 = __importDefault(require("@sqlite.org/sqlite-wasm")); 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 sqlite3 = await (0, sqlite_wasm_1.default)(); let db; if (sqlite3.oo1.OpfsDb) { const dbName = dbFilename.startsWith(':') ? dbFilename : ['ocap', (0, env_ts_1.getDBFolder)(), dbFilename].filter(Boolean).join('-'); db = new sqlite3.oo1.OpfsDb(dbName, 'cw'); } else { logger?.warn(`OPFS not enabled, database will be ephemeral`); db = new sqlite3.oo1.DB(`:memory:`, 'cw'); } const dbWithTx = db; dbWithTx._inTx = false; dbWithTx._spStack = []; return dbWithTx; } /** * Makes a {@link KVStore} on top of a SQLite database * * @param db - The (open) database to use. * @param logger - A logger object for recording activity. * @returns A key/value store using the given database. */ function makeKVStore(db, logger) { db.exec(common_ts_1.SQL_QUERIES.CREATE_TABLE); const sqlKVGet = db.prepare(common_ts_1.SQL_QUERIES.GET); /** * 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) { sqlKVGet.bind([key]); if (sqlKVGet.step()) { const result = sqlKVGet.getString(0); if (result) { sqlKVGet.reset(); logger?.debug(`kv get '${key}' as '${result}'`); return result; } } sqlKVGet.reset(); if (required) { throw Error(`no record matching key '${key}'`); } return undefined; } const sqlKVGetNextKey = db.prepare(common_ts_1.SQL_QUERIES.GET_NEXT); /** * 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) { sqlKVGetNextKey.bind([previousKey]); if (sqlKVGetNextKey.step()) { const result = sqlKVGetNextKey.getString(0); if (result) { sqlKVGetNextKey.reset(); logger?.debug(`kv getNextKey '${previousKey}' as '${result}'`); return result; } } sqlKVGetNextKey.reset(); return undefined; } 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) { logger?.debug(`kv set '${key}' to '${value}'`); sqlKVSet.bind([key, value]); sqlKVSet.step(); sqlKVSet.reset(); } 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) { logger?.debug(`kv delete '${key}'`); sqlKVDelete.bind([key]); sqlKVDelete.step(); sqlKVDelete.reset(); } 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 A key/value store to base higher level stores on. */ async function makeSQLKernelDatabase({ dbFilename, logger, }) { const db = await initDB(dbFilename ?? common_ts_1.DEFAULT_DB_FILENAME, logger); logger?.debug('Initializing kernel store'); const kvStore = makeKVStore(db, logger?.subLogger({ tags: ['kv'] })); db.exec(common_ts_1.SQL_QUERIES.CREATE_TABLE_VS); 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._inTx) { return false; } sqlBeginTransaction.step(); sqlBeginTransaction.reset(); db._inTx = true; return true; } /** * Commit a transaction if one is active and no savepoints remain */ function commitIfNeeded() { if (db._inTx && db._spStack.length === 0) { sqlCommitTransaction.step(); sqlCommitTransaction.reset(); db._inTx = false; } } /** * Rollback a transaction */ function rollbackIfNeeded() { if (db._inTx) { sqlAbortTransaction.step(); sqlAbortTransaction.reset(); db._inTx = false; db._spStack.length = 0; } } /** * Safely mutate the database with proper transaction management * * @param mutator - Function that performs the database mutations */ function safeMutate(mutator) { const startedTx = beginIfNeeded(); try { mutator(); if (startedTx) { commitIfNeeded(); } } catch (error) { if (startedTx) { rollbackIfNeeded(); } throw error; } } /** * Delete everything from the database. */ function kvClear() { logger?.debug('clearing all kernel state'); sqlKVClear.step(); sqlKVClear.reset(); sqlKVClearVS.step(); sqlKVClearVS.reset(); } /** * Execute a SQL query. * * @param sql - The SQL query to execute. * @returns An array of results. */ function executeQuery(sql) { const stmt = db.prepare(sql); const results = []; try { const { columnCount } = stmt; while (stmt.step()) { const row = {}; for (let i = 0; i < columnCount; i++) { const columnName = stmt.getColumnName(i); if (columnName) { row[columnName] = String(stmt.get(i)); } } results.push(row); } } finally { stmt.reset(); } return results; } /** * 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 = []; sqlVatstoreGetAll.bind([vatID]); try { while (sqlVatstoreGetAll.step()) { const key = sqlVatstoreGetAll.getString(0); const value = sqlVatstoreGetAll.getString(1); result.push([key, value]); } } finally { sqlVatstoreGetAll.reset(); } 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) { safeMutate(() => { for (const [key, value] of sets) { sqlVatstoreSet.bind([vatID, key, value]); sqlVatstoreSet.step(); sqlVatstoreSet.reset(); } for (const value of deletes) { sqlVatstoreDelete.bind([vatID, value]); sqlVatstoreDelete.step(); sqlVatstoreDelete.reset(); } }); } return { getKVData, updateKVData, }; } /** * Delete an entire VatStore. * * @param vatId - The vat whose store is to be deleted. */ function deleteVatStore(vatId) { sqlVatstoreDeleteAll.bind([vatId]); sqlVatstoreDeleteAll.step(); sqlVatstoreDeleteAll.reset(); } /** * 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, clear: kvClear, executeQuery, makeVatStore, deleteVatStore, createSavepoint, rollbackSavepoint, releaseSavepoint, }; } //# sourceMappingURL=wasm.cjs.map