@x5e/gink
Version:
an eventually consistent database
961 lines • 43.4 kB
JavaScript
;
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