@metamask/kernel-store
Version:
Ocap Kernel storage abstractions and implementations
364 lines • 11.9 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.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