UNPKG

@x5e/gink

Version:

an eventually consistent database

209 lines 9.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Keyed = void 0; const Container_1 = require("./Container"); const utils_1 = require("./utils"); const factories_1 = require("./factories"); const store_utils_1 = require("./store_utils"); const builders_1 = require("./builders"); class Keyed extends Container_1.Container { /** * Sets a key/value association in a directory. * If a bundler is supplied, the function will add the entry to that bundler * and return immediately (you'll need to addBundler manually in that case). * If the caller does not supply a bundler, then one is created on the fly, and * then this method will await on the bundler being added to the database instance. * This is to allow simple console usage like: * await myDirectory.set("foo", "bar"); * @param key * @param value * @param change an optional bundler to put this in. * @returns a promise that resolves to the address of the newly created entry */ set(key, value, meta) { if (value === undefined) { throw new Error("value is undefined"); } return this.addEntry(key, value, meta); } /** * Adds a deletion marker (tombstone) for a particular key in the directory. * The corresponding value will be seen to be unset in the data model. * @param key * @param change an optional bundler to put this in. * @returns a promise that resolves to the address of the newly created deletion entry */ async delete(key, meta) { return this.addEntry(key, Container_1.Container.DELETION, meta); } /** * Returns a promise that resolves to the most recent value set for the given key, or undefined. * @param key * @param asOf * @returns undefined, a basic value, or a container */ async get(key, asOf) { const entry = await this.database.store.getEntryByKey(this.address, key, asOf); return (0, factories_1.interpret)(entry, this.database); } async size(asOf) { const entries = await this.database.store.getKeyedEntries(this.address, asOf); return entries.size; } async has(key, asOf) { const result = await this.database.store.getEntryByKey(this.address, key, asOf); if (result !== undefined && result.deletion) { return false; } return result !== undefined; } async reset(toTime, recurse, meta) { if (this.behavior === builders_1.Behavior.PROPERTY) { throw new Error("Cannot directly reset a property. " + "Calling reset on a Container will reset its properties."); } if (recurse === true) { recurse = new Set(); } if (recurse instanceof Set) { recurse.add((0, utils_1.muidToString)(this.address)); } const bundler = await this.database.startBundle(meta); if (!toTime) { // If no time is specified, we are resetting to epoch, which is just a clear this.clear(false, { bundler }); } else { const keys = new Set(); const thenEntries = await this.database.store.getKeyedEntries(this.address, toTime); for (const [key, entry] of thenEntries) { keys.add(entry.storageKey); } const nowEntries = await this.database.store.getKeyedEntries(this.address); for (const [key, entry] of nowEntries) { keys.add(entry.storageKey); } for (const key of keys) { const genericKey = (0, utils_1.fromStorageKey)(key); const thenEntry = await this.database.store.getEntryByKey(this.address, genericKey, toTime); const thenValue = thenEntry?.pointeeList.length > 0 ? await (0, factories_1.construct)((0, utils_1.muidTupleToMuid)(thenEntry.pointeeList[0]), this.database) : thenEntry?.value; const nowEntry = await this.database.store.getEntryByKey(this.address, genericKey); const nowValue = nowEntry?.pointeeList.length > 0 ? await (0, factories_1.construct)((0, utils_1.muidTupleToMuid)(nowEntry.pointeeList[0]), this.database) : nowEntry?.value; if (!nowEntry) { // This key was present then, but not now, so we need to add it back if (thenEntry && !thenEntry.deletion) { (0, utils_1.ensure)(thenValue, `missing for key: ${key}, ${JSON.stringify(genericKey)}`); await this.addEntry(genericKey, thenValue, { bundler }); } } else if (!thenEntry) { // This key is present now, but not then, so we need to delete it (0, utils_1.ensure)(nowEntry && nowValue, "missing value?"); await this.addEntry(genericKey, Container_1.Container.DELETION, { bundler, }); } else { // Present both then and now. Check if the values are different if (nowValue !== thenValue) { // Make sure the values are not the same container if (!(nowValue instanceof Container_1.Container && thenValue instanceof Container_1.Container && (0, utils_1.muidToString)(nowValue.address) === (0, utils_1.muidToString)(thenValue.address))) { // Update the entry await this.addEntry(genericKey, thenValue, { bundler, }); } } } if (recurse && thenValue instanceof Container_1.Container && !recurse.has((0, utils_1.muidToString)(thenValue.address))) { await thenValue.reset(toTime, recurse, { bundler }); } } } if (!meta?.bundler) { await bundler.commit(); } } /** * Dumps the contents of this directory into a javascript Map; mostly useful for * debugging though also could be used to create a backup of a database. * @param asOf effective time to get the dump for, or undefined for the present * @returns a javascript map from keys (numbers or strings) to values or containers */ async toMap(asOf) { const entries = await this.database.store.getKeyedEntries(this.address, asOf); const resultMap = new Map(); for (const [key, entry] of entries) { const pointee = entry.pointeeList.length > 0 ? (0, utils_1.muidTupleToMuid)(entry.pointeeList[0]) : undefined; const val = entry.value !== undefined ? entry.value : await (0, factories_1.construct)(pointee, this.database); resultMap.set(entry.storageKey, val); } return resultMap; } /** * Generates a JSON representation of the data in this container. * Mostly intended for demo/debug purposes. * @param indent true to pretty print * @param asOf effective time * @param seen (internal use only! This prevents cycles from breaking things) * @returns a JSON string */ async toJson(indent = false, asOf, seen) { (0, utils_1.ensure)(indent === false, "indent not implemented"); if (seen === undefined) seen = new Set(); const mySig = (0, utils_1.muidToString)(this.address); if (seen.has(mySig)) return "null"; seen.add(mySig); const asMap = await this.toMap(asOf); let returning = "{"; let first = true; const entries = (await this.database.store.getKeyedEntries(this.address, asOf)).values(); for (const entry of entries) { if (first) { first = false; } else { returning += ","; } const storageKey = entry.storageKey; if (typeof storageKey === "string") { returning += JSON.stringify(storageKey); } else if (storageKey instanceof Uint8Array) { returning += '"' + storageKey.toString() + '"'; } else { returning += JSON.stringify((0, store_utils_1.storageKeyToString)(storageKey)); } returning += ":"; if (entry.value !== undefined) { returning += (0, utils_1.valueToJson)(entry.value); } else if (entry.pointeeList.length > 0) { returning += await (await (0, factories_1.construct)((0, utils_1.muidTupleToMuid)(entry.pointeeList[0]), this.database)).toJson(indent === false ? false : +indent + 1, asOf, seen); } else { throw new Error(`don't know how to interpret: ${entry}`); } } returning += "}"; return returning; } } exports.Keyed = Keyed; //# sourceMappingURL=Keyed.js.map