UNPKG

@x5e/gink

Version:

an eventually consistent database

961 lines 43.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IndexedDbStore = void 0; const lodash_1 = require("lodash"); const utils_1 = require("./utils"); const idb_1 = require("idb"); const store_utils_1 = require("./store_utils"); const builders_1 = require("./builders"); const PromiseChainLock_1 = require("./PromiseChainLock"); const Retrieval_1 = require("./Retrieval"); if (eval("typeof indexedDB") === "undefined") { // ts-node has problems with typeof eval('require("fake-indexeddb/auto");'); // hide require from webpack } /** * Uses an indexedDb to implement the Store interface. On the server side, this will * be done using a shim that is only an in-memory implementation of the IndexedDb API, * so the LogBackedStore should be used on the server for persistence. Most of the time * uses of Gink should not need to call methods on the store directly, instead just * pass it into the Database (or SimpleServer, etc.). */ class IndexedDbStore { constructor(indexedDbName, reset, keepingHistory = true) { this.keepingHistory = keepingHistory; this.transaction = null; this.countTrxns = 0; this.initialized = false; this.processingLock = new PromiseChainLock_1.PromiseChainLock(); this.lastCaller = ""; this.foundBundleCallBacks = []; this.pending = []; this.ready = this.initialize(indexedDbName, reset); } async initialize(indexedDbName, reset) { await utils_1.librariesReady; if (reset) { await (0, idb_1.deleteDB)(indexedDbName, { blocked() { const msg = `Unable to delete IndexedDB database ${indexedDbName} !!!`; throw new Error(msg); }, }); } this.wrapped = await (0, idb_1.openDB)(indexedDbName, 1, { upgrade(db, _oldVersion, _newVersion, _transaction) { // info(`upgrade, oldVersion:${oldVersion}, newVersion:${newVersion}`); /* The object store for transactions will store the raw bytes received for each transaction to avoid dropping unknown fields. Since this isn't a javascript object, we'll use [timestamp, medallion] to keep transactions ordered in time. */ db.createObjectStore("trxns"); // a map from BundleKey to BundleBytes /* Stores ChainInfo objects. This will keep track of which transactions have been processed per chain. */ db.createObjectStore("chainInfos", { keyPath: ["medallion", "chainStart"], }); /* Keep track of active chains this instance can write to. It stores objects with two keys: "medallion" and "chainStart", which have value Medallion and ChainStart respectively. This could alternatively be implemented with a keys being medallions and values being chainStarts, but this is a little easier because the getAll() interface is a bit nicer than working with the cursor interface. */ db.createObjectStore("activeChains", { keyPath: ["claimTime"], }); /* Keep track of the identities of who started each chain. key: [medallion, chainStart] value: identity (string) Not setting keyPath since [medallion, chainStart] can't be pulled from the value */ db.createObjectStore("identities"); db.createObjectStore("verifyKeys"); db.createObjectStore("secretKeys"); db.createObjectStore("symmetricKeys"); db.createObjectStore("clearances", { keyPath: ["containerId", "clearanceId"], }); db.createObjectStore("containers"); // map from AddressTuple to ContainerBytes // the "removals" stores objects of type `Removal` const removals = db.createObjectStore("removals", { keyPath: "removalId", }); removals.createIndex("by-container-movement", [ "containerId", "removalId", ]); removals.createIndex("by-removing", ["removing", "removalId"]); // The "entries" store has objects of type Entry (from typedefs) const entries = db.createObjectStore("entries", { keyPath: "placementId", }); entries.createIndex("by-container-key-placement", [ "containerId", "storageKey", "placementId", ]); entries.createIndex("by-container-name", [ "containerId", "value", ]); // Useful for quickly looking up a container by its name // This index is used to find all properties that describe a particular container. entries.createIndex("by-key-placement", [ "storageKey", "placementId", ]); // ideally the next three indexes would be partial indexes, covering only sequences and edges // it might be worth pulling them out into separate lookup tables. entries.createIndex("locations", ["entryId", "placementId"]); entries.createIndex("sources", [ "sourceList", "storageKey", "placementId", ]); entries.createIndex("targets", [ "targetList", "storageKey", "placementId", ]); }, }); this.initialized = true; } async getVerifyKey(chainInfo) { await this.ready; const wrappedTransaction = this.getTransaction(); const verifyKey = await wrappedTransaction .objectStore("verifyKeys") .get(chainInfo); return verifyKey; } async saveKeyPair(keyPair) { await this.ready; const trxn = this.getTransaction(); await trxn .objectStore("secretKeys") .put(keyPair.secretKey, keyPair.publicKey); } async pullKeyPair(publicKey) { await this.ready; const trxn = this.getTransaction(); const secretKey = await trxn.objectStore("secretKeys").get(publicKey); return { secretKey, publicKey }; } async saveSymmetricKey(symmetricKey) { if (symmetricKey.length !== 32) { throw new Error("symmetric key must be 32 bytes"); } await this.ready; const keyId = (0, utils_1.shorterHash)(symmetricKey); const trxn = this.getTransaction(); await trxn.objectStore("symmetricKeys").put(symmetricKey, keyId); return keyId; } async getSymmetricKey(keyId, trxn) { await this.ready; trxn = trxn !== null && trxn !== void 0 ? trxn : this.getTransaction(); return await trxn.objectStore("symmetricKeys").get(keyId); } clearTransaction() { this.transaction = null; } getTransaction() { const stackString = new Error().stack; const callerLine = stackString ? stackString.split("\n")[2] : ""; if (this.transaction === null || this.lastCaller !== callerLine) { this.lastCaller = callerLine; this.countTrxns += 1; this.transaction = this.wrapped.transaction([ "entries", "clearances", "removals", "trxns", "chainInfos", "activeChains", "containers", "identities", "verifyKeys", "secretKeys", "symmetricKeys", ], "readwrite"); this.transaction.done.finally(() => this.clearTransaction()); } return this.transaction; } getTransactionCount() { return this.countTrxns; } async dropHistory(container, before) { const beforeTs = before ? await this.asOfToTimestamp(before) : (0, utils_1.generateTimestamp)(); const trxn = this.wrapped.transaction(["removals", "entries"], "readwrite"); let removalsCursor = await trxn .objectStore("removals") .openCursor(IDBKeyRange.upperBound([beforeTs])); if (container) { const containerTuple = (0, utils_1.muidToTuple)(container); const range = IDBKeyRange.bound([containerTuple, [0]], [containerTuple, [beforeTs]]); removalsCursor = await trxn .objectStore("removals") .index("by-container-movement") .openCursor(range); } while (removalsCursor) { await trxn .objectStore("entries") .delete(removalsCursor.value.removing); await removalsCursor.delete(); removalsCursor = await removalsCursor.continue(); } return trxn.done; } async stopHistory() { this.keepingHistory = false; return this.dropHistory(); } startHistory() { this.keepingHistory = true; } async close() { try { await this.ready; } finally { if (this.wrapped) { this.wrapped.close(); } } } async getLocation(entry, asOf) { const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : (0, utils_1.generateTimestamp)(); const trxn = this.wrapped.transaction(["entries", "clearances", "removals"], "readonly"); const range = IDBKeyRange.bound([(0, utils_1.muidToTuple)(entry), [0]], [(0, utils_1.muidToTuple)(entry), [Infinity]]); let cursor = await trxn .objectStore("entries") .index("locations") .openCursor(range, "prev"); if (cursor && cursor.value) { const containerId = cursor.value.containerId; const placementId = cursor.value.placementId; const entryId = cursor.value.entryId; const lastClear = await this.getClearanceTime(trxn, containerId, asOfTs); const removalLower = [entryId]; const removalUpper = [entryId, [asOfTs]]; const removalCursor = await trxn .objectStore("removals") .index("by-removing") .openCursor(IDBKeyRange.bound(removalLower, removalUpper), "prev"); const foundRemoval = removalCursor && removalCursor.value; if (lastClear > placementId[0] || foundRemoval) return undefined; return { container: containerId, key: cursor.value.storageKey, placement: placementId, }; } return undefined; } async asOfToTimestamp(asOf) { if (asOf instanceof Date) { return asOf.getTime() * 1000; } if (asOf > IndexedDbStore.YEAR_2020) { return asOf; } if (asOf < 0 && asOf > -1000) { // Interpret as number of bundles in the past. let cursor = await this.wrapped .transaction("trxns", "readonly") .objectStore("trxns") .openCursor(undefined, "prev"); let bundlesToTraverse = -asOf; for (; cursor; cursor = await cursor.continue()) { if (--bundlesToTraverse === 0) { const tuple = cursor.key; return tuple[0]; } } // Looking further back then we have bundles. throw new Error("no bundles that far back"); } throw new Error(`don't know how to interpret asOf=${asOf}`); } async getClaimedChains() { if (!this.initialized) throw new Error("not initilized"); const objectStore = this.wrapped .transaction("activeChains", "readonly") .objectStore("activeChains"); const items = await objectStore.getAll(); const result = new Map(); let lastTs = 0; for (let item of items) { if (item.claimTime < lastTs) throw new Error("claims not in order"); lastTs = item.claimTime; result.set(item.medallion, item); } return result; } async getChainIdentity(chainInfo) { await this.ready; const wrappedTransaction = this.getTransaction(); const identity = await wrappedTransaction .objectStore("identities") .get(chainInfo); return identity; } async claimChain(medallion, chainStart, actorId, transaction) { await this.ready; const wrappedTransaction = transaction !== null && transaction !== void 0 ? transaction : this.getTransaction(); const claim = { chainStart, medallion, actorId: actorId || 0, claimTime: (0, utils_1.generateTimestamp)(), }; await wrappedTransaction.objectStore("activeChains").add(claim); return claim; } async getChainTracker() { await this.ready; const chainInfos = await this.getChainInfos(); const chainTracker = (0, store_utils_1.buildChainTracker)(chainInfos); return chainTracker; } async getChainInfos() { await this.ready; return await this.getTransaction().objectStore("chainInfos").getAll(); } addBundle(bundleView, claimChain) { if (!this.initialized) throw new Error("need to await on store.ready"); return this.processingLock .acquireLock() .then(async (unlock) => { const trxn = this.getTransaction(); let added = false; try { added = await this.addBundleHelper(trxn, bundleView, claimChain); } finally { unlock(); } await trxn.done; return added; }) .catch((e) => { throw e; }); } async addBundleHelper(trxn, bundleView, claimChain) { const bundleInfo = bundleView.info; const bundleBuilder = bundleView.builder; const { timestamp, medallion, chainStart, priorTime } = bundleInfo; const oldChainInfo = await trxn .objectStore("chainInfos") .get([medallion, chainStart]); if (oldChainInfo || priorTime) { if ((oldChainInfo === null || oldChainInfo === void 0 ? void 0 : oldChainInfo.timestamp) >= timestamp) { return false; } if ((oldChainInfo === null || oldChainInfo === void 0 ? void 0 : oldChainInfo.timestamp) !== priorTime) { //TODO(https://github.com/google/gink/issues/27): Need to explicitly close? throw new Error(`missing ${JSON.stringify(bundleInfo)}, have ${JSON.stringify(oldChainInfo)}`); } const priorHash = bundleBuilder.getPriorHash(); if (!priorHash || priorHash.length != 32 || !(0, utils_1.sameData)(priorHash, oldChainInfo.hashCode)) throw new Error("prior hash is invalid"); } const identity = bundleBuilder.getIdentity(); // If this is a new chain, save the identity & claim this chain if (claimChain) { (0, utils_1.ensure)(bundleInfo.timestamp === bundleInfo.chainStart, "timestamp !== chainstart"); (0, utils_1.ensure)(identity, "identity required to start a chain"); await this.claimChain(bundleInfo.medallion, bundleInfo.chainStart, (0, utils_1.getActorId)(), trxn); } let verifyKey; const chainInfo = [ bundleInfo.medallion, bundleInfo.chainStart, ]; if (bundleInfo.chainStart === bundleInfo.timestamp) { (0, utils_1.ensure)(identity, `identity required to start a chain`); await trxn.objectStore("identities").add(identity, chainInfo); verifyKey = bundleBuilder.getVerifyKey(); await trxn.objectStore("verifyKeys").put(verifyKey, chainInfo); } else { (0, utils_1.ensure)(!identity, `cannot have identity in non-chain-start bundle - ${identity}`); verifyKey = await trxn.objectStore("verifyKeys").get(chainInfo); } (0, utils_1.verifyBundle)(bundleView.bytes, verifyKey); await trxn.objectStore("chainInfos").put(bundleInfo); // Only timestamp and medallion are required for uniqueness, the others just added to make // the getNeededTransactions faster by not requiring parsing again. const bundleKey = (0, store_utils_1.bundleInfoToKey)(bundleInfo); await trxn.objectStore("trxns").add(bundleView.bytes, bundleKey); // Decrypt bundle const encrypted = bundleBuilder.getEncrypted(); let changesList; if (encrypted) { const keyId = bundleBuilder.getKeyId(); if (bundleBuilder.getChangesList().length > 0) { throw new Error("did not expect plain changes when using encryption"); } if (!keyId) { throw new Error("expected keyId with encrypted bundle"); } const symmetricKey = (0, utils_1.ensure)(await this.getSymmetricKey(keyId, trxn), "could not find symmetric key referenced in bundle"); const decrypted = (0, utils_1.decryptMessage)(encrypted, symmetricKey); const innerBundleBuilder = (builders_1.BundleBuilder.deserializeBinary(decrypted)); changesList = innerBundleBuilder.getChangesList(); } // Changes list will either come from getChangesList in an unencrypted bundle, or // getChangesList from the decrypted inner bundle. if (!changesList) changesList = bundleBuilder.getChangesList(); for (let index = 0; index < changesList.length; index++) { const offset = index + 1; const changeBuilder = changesList[index]; (0, utils_1.ensure)(offset > 0); const changeAddressTuple = [ timestamp, medallion, offset, ]; const changeAddress = { timestamp, medallion, offset }; if (changeBuilder.hasContainer()) { const containerBytes = changeBuilder .getContainer() .serializeBinary(); await trxn .objectStore("containers") .add(containerBytes, changeAddressTuple); continue; } if (changeBuilder.hasEntry()) { const entryBuilder = changeBuilder.getEntry(); let containerId = [0, 0, 0]; if (entryBuilder.hasContainer()) { containerId = (0, store_utils_1.extractContainerMuid)(entryBuilder, bundleInfo); } const storageKey = (0, store_utils_1.getStorageKey)(entryBuilder, changeAddress); const entryId = [timestamp, medallion, offset]; const behavior = entryBuilder.getBehavior(); const placementId = entryId; let pointeeList = []; if (entryBuilder.hasPointee()) { pointeeList = (0, store_utils_1.buildPointeeList)(entryBuilder, bundleInfo); } let sourceList = []; let targetList = []; if (entryBuilder.hasPair()) { [sourceList, targetList] = (0, store_utils_1.buildPairLists)(entryBuilder, bundleInfo); } const value = entryBuilder.hasValue() ? (0, utils_1.unwrapValue)(entryBuilder.getValue()) : undefined; const expiry = entryBuilder.getExpiry() || undefined; const deletion = entryBuilder.getDeletion(); const entry = { behavior, containerId, storageKey, entryId, pointeeList, value, expiry, deletion, placementId, sourceList, targetList, }; if (!(behavior === builders_1.Behavior.SEQUENCE || behavior === builders_1.Behavior.EDGE_TYPE)) { const range = IDBKeyRange.bound([containerId, storageKey], [containerId, storageKey, placementId]); const search = await trxn .objectStore("entries") .index("by-container-key-placement") .openCursor(range, "prev"); if (search) { if (this.keepingHistory) { const removal = { removing: search.value.placementId, removalId: placementId, containerId: containerId, dest: 0, entryId: search.value.entryId, }; await trxn.objectStore("removals").add(removal); } else { await trxn .objectStore("entries") .delete(search.value.placementId); } } } await trxn.objectStore("entries").add(entry); continue; } if (changeBuilder.hasMovement()) { const movement = (0, store_utils_1.extractMovement)(changeBuilder, bundleInfo, offset); const { entryId, movementId, containerId, dest, purge } = movement; const range = IDBKeyRange.bound([entryId, [0]], [entryId, [Infinity]]); const search = await trxn .objectStore("entries") .index("locations") .openCursor(range, "prev"); if (!search) { continue; // Nothing found to remove. } const found = search.value; if (dest !== 0) { const destEntry = { behavior: found.behavior, containerId: found.containerId, storageKey: dest, entryId: found.entryId, pointeeList: found.pointeeList, value: found.value, expiry: found.expiry, deletion: found.deletion, placementId: movementId, sourceList: found.sourceList, targetList: found.targetList, }; await trxn.objectStore("entries").add(destEntry); } if (purge || !this.keepingHistory) { search.delete(); } else { const removal = { containerId, removalId: movementId, dest, entryId, removing: found.placementId, }; await trxn.objectStore("removals").add(removal); } continue; } if (changeBuilder.hasClearance()) { const clearanceBuilder = changeBuilder.getClearance(); const container = (0, utils_1.builderToMuid)(clearanceBuilder.getContainer(), { timestamp, medallion, offset }); const containerMuidTuple = [ container.timestamp, container.medallion, container.offset, ]; if (clearanceBuilder.getPurge()) { // When purging, remove all entries from the container. const onePast = [ container.timestamp, container.medallion, container.offset + 1, ]; const range = IDBKeyRange.bound([containerMuidTuple], [onePast], false, true); let entriesCursor = await trxn .objectStore("entries") .index("by-container-key-placement") .openCursor(range); while (entriesCursor) { await entriesCursor.delete(); entriesCursor = await entriesCursor.continue(); } // When doing a purging clear, remove previous clearances for the container. let clearancesCursor = await trxn .objectStore("clearances") .openCursor(range); while (clearancesCursor) { await clearancesCursor.delete(); clearancesCursor = await clearancesCursor.continue(); } // When doing a purging clear, remove all removals for the container. let removalsCursor = await trxn .objectStore("removals") .index("by-container-movement") .openCursor(range); while (removalsCursor) { await removalsCursor.delete(); removalsCursor = await removalsCursor.continue(); } } const clearance = { containerId: containerMuidTuple, clearanceId: changeAddressTuple, purging: clearanceBuilder.getPurge(), }; await trxn.objectStore("clearances").add(clearance); continue; } throw new Error("don't know how to apply this kind of change"); } return true; } async getContainerBytes(address) { const addressTuple = [ address.timestamp, address.medallion, address.offset, ]; return await this.wrapped .transaction("containers", "readonly") .objectStore("containers") .get(addressTuple); } async getEntryByKey(container, key, asOf) { var _a, _b, _c; const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : Infinity; const desiredSrc = [ (_a = container === null || container === void 0 ? void 0 : container.timestamp) !== null && _a !== void 0 ? _a : 0, (_b = container === null || container === void 0 ? void 0 : container.medallion) !== null && _b !== void 0 ? _b : 0, (_c = container === null || container === void 0 ? void 0 : container.offset) !== null && _c !== void 0 ? _c : 0, ]; const trxn = this.wrapped.transaction(["clearances", "entries"], "readonly"); let clearanceTime = 0; const clearancesSearch = IDBKeyRange.bound([desiredSrc], [desiredSrc, [asOfTs]]); const clearancesCursor = await trxn .objectStore("clearances") .openCursor(clearancesSearch, "prev"); if (clearancesCursor) { clearanceTime = clearancesCursor.value.clearanceId[0]; } let upperTuple = [asOfTs]; const storageKey = (0, store_utils_1.toStorageKey)(key); const lower = [desiredSrc]; const upper = [desiredSrc, storageKey, upperTuple]; const searchRange = IDBKeyRange.bound(lower, upper); const entriesCursor = await trxn .objectStore("entries") .index("by-container-key-placement") .openCursor(searchRange, "prev"); if (entriesCursor) { const entry = entriesCursor.value; if (!(0, utils_1.sameData)(entry.storageKey, storageKey)) { return undefined; } if (entry.placementId[0] < clearanceTime) { // a clearance happened after this thing was placed, so treat it as gone return undefined; } return entry; } return undefined; } async getClearanceTime(trxn, muidTuple, asOfTs) { const clearancesSearch = IDBKeyRange.bound([muidTuple], [muidTuple, [asOfTs]]); const clearancesCursor = await trxn .objectStore("clearances") .openCursor(clearancesSearch, "prev"); if (clearancesCursor) { return clearancesCursor.value.clearanceId[0]; } return 0; } async getKeyedEntries(container, asOf) { var _a, _b, _c; const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : Infinity; const desiredSrc = [ (_a = container === null || container === void 0 ? void 0 : container.timestamp) !== null && _a !== void 0 ? _a : 0, (_b = container === null || container === void 0 ? void 0 : container.medallion) !== null && _b !== void 0 ? _b : 0, (_c = container === null || container === void 0 ? void 0 : container.offset) !== null && _c !== void 0 ? _c : 0, ]; const trxn = this.wrapped.transaction(["clearances", "entries"], "readonly"); const clearanceTime = await this.getClearanceTime(trxn, desiredSrc, asOfTs); const lower = [desiredSrc]; const searchRange = IDBKeyRange.lowerBound(lower); let cursor = await trxn .objectStore("entries") .index("by-container-key-placement") .openCursor(searchRange, "next"); const result = new Map(); for (; cursor && (0, utils_1.matches)(cursor.key[0], desiredSrc); cursor = await cursor.continue()) { const entry = cursor.value; (0, utils_1.ensure)(entry.behavior === builders_1.Behavior.DIRECTORY || entry.behavior === builders_1.Behavior.KEY_SET || entry.behavior === builders_1.Behavior.GROUP || entry.behavior === builders_1.Behavior.PAIR_SET || entry.behavior === builders_1.Behavior.PAIR_MAP || entry.behavior === builders_1.Behavior.PROPERTY); const key = (0, store_utils_1.storageKeyToString)(entry.storageKey); if (entry.entryId[0] < asOfTs && entry.entryId[0] >= clearanceTime) { if (entry.deletion) { result.delete(key); } else { result.set(key, entry); } } } return result; } async getEntriesBySourceOrTarget(vertex, source, asOf) { await this.ready; const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : (0, utils_1.generateTimestamp)() + 1; const indexable = (0, utils_1.dehydrate)(vertex); const trxn = this.wrapped.transaction(["clearances", "entries", "removals"], "readonly"); const clearanceTime = await this.getClearanceTime(trxn, indexable, asOfTs); const lower = [[indexable], -Infinity]; const upper = [[indexable], +Infinity]; const searchRange = IDBKeyRange.bound(lower, upper); let entriesCursor = await trxn .objectStore("entries") .index(source ? "sources" : "targets") .openCursor(searchRange); const returning = []; const removals = trxn.objectStore("removals"); for (; entriesCursor; entriesCursor = await entriesCursor.continue()) { const entry = entriesCursor.value; if (entry.placementId[0] >= asOfTs || entry.placementId[0] < clearanceTime) continue; const removalsBound = IDBKeyRange.bound([entry.placementId], [entry.placementId, [asOfTs]]); // TODO: This seek-per-entry isn't very efficient and should be a replaced with a scan. const removalsCursor = await removals .index("by-removing") .openCursor(removalsBound); if (!removalsCursor) returning.push(entry); } return returning; } /** * Returns entry data for a List. Does it in a single pass rather than using an async generator * because if a user tried to await on something else between entries it would cause the IndexedDb * transaction to auto-close. * @param container to get entries for * @param through number to get, negative for starting from end * @param asOf show results as of a time in the past * @returns a promise of a list of ChangePairs */ async getOrderedEntries(container, through = Infinity, asOf) { var _a, _b, _c; const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : (0, utils_1.generateTimestamp)() + 1; const containerId = [ (_a = container === null || container === void 0 ? void 0 : container.timestamp) !== null && _a !== void 0 ? _a : 0, (_b = container === null || container === void 0 ? void 0 : container.medallion) !== null && _b !== void 0 ? _b : 0, (_c = container === null || container === void 0 ? void 0 : container.offset) !== null && _c !== void 0 ? _c : 0, ]; const lower = [containerId, 0]; const upper = [containerId, asOfTs]; const range = IDBKeyRange.bound(lower, upper); const trxn = this.wrapped.transaction(["clearances", "entries", "removals"], "readonly"); let clearanceTime = 0; const clearancesSearch = IDBKeyRange.bound([containerId], [containerId, [asOfTs]]); const clearancesCursor = await trxn .objectStore("clearances") .openCursor(clearancesSearch, "prev"); if (clearancesCursor) { clearanceTime = clearancesCursor.value.clearanceId[0]; } const entries = trxn.objectStore("entries"); const removals = trxn.objectStore("removals"); const returning = new Map(); let entriesCursor = await entries .index("by-container-key-placement") .openCursor(range, through < 0 ? "prev" : "next"); const needed = through < 0 ? -through : through + 1; while (entriesCursor && returning.size < needed) { const entry = entriesCursor.value; if (entry.placementId[0] >= clearanceTime) { const removalsBound = IDBKeyRange.bound([entry.placementId], [entry.placementId, [asOfTs]]); // TODO: This seek-per-entry isn't very efficient and should be a replaced with a scan. const removalsCursor = await removals .index("by-removing") .openCursor(removalsBound); if (!removalsCursor) { const placementIdStr = (0, utils_1.muidTupleToString)(entry.placementId); const returningKey = `${entry.storageKey},${placementIdStr}`; returning.set(returningKey, entry); } } entriesCursor = await entriesCursor.continue(); } return returning; } async getEntryById(entryMuid, asOf) { var _a, _b, _c; const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : (0, utils_1.generateTimestamp)(); const entryId = [ (_a = entryMuid.timestamp) !== null && _a !== void 0 ? _a : 0, (_b = entryMuid.medallion) !== null && _b !== void 0 ? _b : 0, (_c = entryMuid.offset) !== null && _c !== void 0 ? _c : 0, ]; const entryRange = IDBKeyRange.bound([entryId, [0]], [entryId, [asOfTs]]); const trxn = this.wrapped.transaction(["entries", "removals", "clearances"], "readonly"); const entryCursor = await trxn .objectStore("entries") .index("locations") .openCursor(entryRange, "prev"); if (!entryCursor) { return undefined; } const entry = entryCursor.value; const lastClear = await this.getClearanceTime(trxn, entry.containerId, asOfTs); if (entry.placementId[0] >= lastClear) { const removalRange = IDBKeyRange.bound([entry.placementId], [entry.placementId, [asOfTs]]); const removalCursor = await trxn .objectStore("removals") .index("by-removing") .openCursor(removalRange); if (!removalCursor) { return entry; } } return undefined; } async getContainersByName(name, asOf) { const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : Infinity; const desiredSrc = [-1, -1, builders_1.Behavior.PROPERTY]; const trxn = this.wrapped.transaction(["clearances", "entries", "removals"], "readonly"); const clearanceTime = await this.getClearanceTime(trxn, desiredSrc, asOfTs); const lower = [desiredSrc, name]; const searchRange = IDBKeyRange.lowerBound(lower); let cursor = await trxn .objectStore("entries") .index("by-container-name") .openCursor(searchRange, "next"); const result = []; for (; cursor && (0, utils_1.matches)(cursor.key[0], desiredSrc) && cursor.key[1] === name; cursor = await cursor.continue()) { const entry = cursor.value; (0, utils_1.ensure)(entry.behavior === builders_1.Behavior.PROPERTY); const range = IDBKeyRange.lowerBound([entry.entryId]); const removal = await trxn .objectStore("removals") .index("by-removing") .openCursor(range); if (removal && removal.value.entryId.toString() === entry.entryId.toString()) { continue; } let key; if (Array.isArray(entry.storageKey) && entry.storageKey.length === 3) { key = entry.storageKey; } (0, utils_1.ensure)(key, "Unexpected storageKey for property: " + entry.storageKey); if (entry.entryId[0] < asOfTs && entry.entryId[0] >= clearanceTime && !entry.deletion) { result.push((0, utils_1.muidTupleToMuid)(key)); } } return result; } async getContainerProperties(containerMuid, asOf) { const asOfTs = asOf ? await this.asOfToTimestamp(asOf) : (0, utils_1.generateTimestamp)(); const containerTuple = (0, utils_1.muidToTuple)(containerMuid); const txn = this.wrapped.transaction(["entries", "clearances"], "readonly"); const range = IDBKeyRange.bound([containerTuple], [containerTuple, [asOfTs]]); let cursor = await txn .objectStore("entries") .index("by-key-placement") .openCursor(range); const result = new Map(); for (; cursor && Array.isArray(cursor.key[0]) && (0, lodash_1.isEqual)(cursor.key[0], containerTuple); cursor = await cursor.continue()) { const entry = cursor.value; // TODO: think about a better way to do this. If there is a group that includes // this container, it may show up here. Though there could only be one entry per group, // so maybe not that big of a deal. if (!(entry.behavior === builders_1.Behavior.PROPERTY)) continue; (0, utils_1.ensure)((0, lodash_1.isEqual)(entry.storageKey, containerTuple)); if (!(Array.isArray(entry.storageKey) && entry.storageKey.length === 3)) { // This is also kinda just to keep typescript happy. // If storageKey is equal to containerMuid, this will never run. throw new Error("Unexpected storageKey for property"); } const clearanceTime = await this.getClearanceTime(txn, (0, utils_1.muidToTuple)((0, utils_1.muidTupleToMuid)(entry.containerId)), asOfTs); if (entry.entryId[0] < asOfTs && entry.entryId[0] >= clearanceTime) { if (entry.deletion) { result.delete((0, utils_1.muidTupleToString)(entry.containerId)); } else { result.set((0, utils_1.muidTupleToString)(entry.containerId), entry.value); } } } return result; } async getAllContainerTuples() { return await this.wrapped .transaction("containers", "readonly") .objectStore("containers") .getAllKeys(); } // for debugging, not part of the api/interface async getAllEntryKeys() { return await this.wrapped .transaction("entries", "readonly") .objectStore("entries") .getAllKeys(); } // for debugging, not part of the api/interface async getAllEntries() { return await this.wrapped .transaction("entries", "readonly") .objectStore("entries") .getAll(); } // for debugging, not part of the api/interface async getAllRemovals() { return await this.wrapped .transaction("removals", "readonly") .objectStore("removals") .getAll(); } // Note the IndexedDB has problems when await is called on anything unrelated // to the current bundle, so its best if `callBack` doesn't await. async getBundles(callBack) { await this.ready; // We loop through all bundles and send those the peer doesn't have. for (let cursor = await this.wrapped .transaction("trxns", "readonly") .objectStore("trxns") .openCursor(); cursor; cursor = await cursor.continue()) { const bundleKey = cursor.key; const bundleInfo = (0, store_utils_1.bundleKeyToInfo)(bundleKey); const bundleBytes = cursor.value; callBack(new Retrieval_1.Retrieval({ bundleBytes, bundleInfo })); } } addFoundBundleCallBack(callback) { this.foundBundleCallBacks.push(callback); } } exports.IndexedDbStore = IndexedDbStore; IndexedDbStore.YEAR_2020 = new Date("2020-01-01").getTime() * 1000; //# sourceMappingURL=IndexedDbStore.js.map