UNPKG

@x5e/gink

Version:

an eventually consistent database

1,026 lines (968 loc) 38.3 kB
import { builderToMuid, ensure, generateTimestamp, muidToString, muidTupleToString, unwrapValue, sameData, getActorId, toLastWithPrefixBeforeSuffix, strToMuid, bytesToHex, verifyBundle, librariesReady, emptyBytes, strToMuidTuple, shorterHash, decryptMessage, dumpTree, } from "./utils"; import { AsOf, BundleInfo, BundleInfoTuple, Bytes, ChainStart, ClaimedChain, Clearance, Entry, Indexable, ScalarKey, Medallion, Muid, MuidTuple, Timestamp, ActorId, BroadcastFunc, Movement, BundleView, KeyPair, Value, Placement, } from "./typedefs"; import { HasMap } from "./HasMap"; import { Store } from "./Store"; import { Behavior, BundleBuilder, ChangeBuilder, EntryBuilder, } from "./builders"; import { MapIterator, TreeMap } from "jstreemap"; import { getStorageKey as getStorageKey, extractMovement, extractContainerMuid, buildPairLists, buildPointeeList, medallionChainStartToString, buildChainTracker, toStorageKey, storageKeyToString, } from "./store_utils"; import { Decomposition } from "./Decomposition"; export class MemoryStore implements Store { ready: Promise<void>; private static readonly YEAR_2020 = new Date("2020-01-01").getTime() * 1000; private foundBundleCallBacks: BroadcastFunc[] = []; // Awkward, but need to use strings to represent objects, since we won't always // have the original reference to use. private trxns: TreeMap<BundleInfoTuple, Uint8Array> = new TreeMap(); // BundleInfoTuple => bytes private chainInfos: TreeMap<string, BundleInfo> = new TreeMap(); // [Medallion, ChainStart] => BundleInfo private activeChains: ClaimedChain[] = []; private clearances: TreeMap<string, Clearance> = new TreeMap(); // ContainerId,ClearanceId => Clearance private containers: TreeMap<string, Uint8Array> = new TreeMap(); // ContainerId => bytes private removals: TreeMap<string, string> = new TreeMap(); // placementId,removalId => "" private placements: TreeMap<string, Entry> = new TreeMap(); // ContainerID,Key,PlacementId => Entry // key-placement index used to find properties by the container they describe (the key) private byKeyPlacement: TreeMap<string, Entry> = new TreeMap(); // Key,PlacementId => Entry private identities: Map<string, string> = new Map(); // Medallion,chainStart => identity private locations: TreeMap<string, string> = new TreeMap(); // entryId,placementId => containerId,key,placementId private byName: TreeMap<string, string> = new TreeMap(); // name,entryMuid => describingMuid private bySource: TreeMap<string, Entry> = new TreeMap(); private byTarget: TreeMap<string, Entry> = new TreeMap(); private verifyKeys: Map<string, Bytes> = new Map(); private secretKeys: Map<string, KeyPair> = new Map(); private symmetricKeys: Map<number, Bytes> = new Map(); // keyId => symmetricKey private accumulatorTotals: Map<string, bigint> = new Map(); constructor(private keepingHistory = true) { this.ready = librariesReady; } getBillionths(muid: Muid, asOf?: AsOf): Promise<bigint> { if (asOf) throw new Error("asOf Not implemented for accumulators yet"); const key = muidToString(muid); return Promise.resolve(this.accumulatorTotals.get(key) ?? BigInt(0)); } saveKeyPair(keyPair: KeyPair): Promise<void> { this.secretKeys.set(bytesToHex(keyPair.publicKey), keyPair); return Promise.resolve(); } pullKeyPair(publicKey: Bytes): Promise<KeyPair> { return Promise.resolve(this.secretKeys.get(bytesToHex(publicKey))); } acquireChain(identity: string): Promise<BundleInfo | null> { return Promise.resolve(null); } dumpEntries() { dumpTree(this.placements); } async saveSymmetricKey(symmetricKey: Bytes): Promise<number> { if (symmetricKey.length !== 32) { throw new Error("symmetric key must be 32 bytes"); } await this.ready; const keyId = shorterHash(symmetricKey); this.symmetricKeys.set(keyId, symmetricKey); return keyId; } async getSymmetricKey(keyId: number): Promise<Bytes> { await this.ready; return this.symmetricKeys.get(keyId); } dropHistory(container?: Muid, before?: AsOf): void { throw new Error("not implemented"); } getVerifyKey(chainInfo: [Medallion, ChainStart]): Promise<Bytes> { return Promise.resolve( this.verifyKeys.get(`${chainInfo[0]},${chainInfo[1]}`), ); } stopHistory(): void { this.keepingHistory = false; this.dropHistory(); } startHistory(): void { this.keepingHistory = true; } getClaimedChains(): Promise<Map<Medallion, ClaimedChain>> { const result = new Map(); let lastTime = 0; this.activeChains.sort((a, b) => a.claimTime - b.claimTime); for (let chain of this.activeChains) { ensure(chain.claimTime > lastTime); lastTime = chain.claimTime; result.set(chain.medallion, chain); } return Promise.resolve(result); } private claimChain( medallion: Medallion, chainStart: ChainStart, actorId?: ActorId, ): Promise<ClaimedChain> { const claim = { medallion, chainStart, actorId: actorId || 0, claimTime: generateTimestamp(), }; this.activeChains.push(claim); return Promise.resolve(claim); } getChainIdentity(chainInfo: [Medallion, ChainStart]): Promise<string> { return Promise.resolve( this.identities.get(`${chainInfo[0]},${chainInfo[1]}`), ); } async getChainTracker(): Promise<HasMap> { const chainInfos = this.getChainInfos(); const chainTracker = buildChainTracker(chainInfos); return Promise.resolve(chainTracker); } private getChainInfos(): Iterable<BundleInfo> { return this.chainInfos.values(); } async addBundle( bundle: BundleView, claimChain?: boolean, ): Promise<boolean> { await this.ready; const bundleBuilder: BundleBuilder = bundle.builder; const bundleInfo = bundle.info; const { timestamp, medallion, chainStart, priorTime } = bundleInfo; const oldChainInfo = this.chainInfos.get( medallionChainStartToString([medallion, chainStart]), ); if (oldChainInfo || priorTime) { if (oldChainInfo?.timestamp >= timestamp) { return false; } if (oldChainInfo?.timestamp !== priorTime) { throw new Error( `missing prior chain entry for ${JSON.stringify(bundleInfo)}, ` + `have ${JSON.stringify(oldChainInfo)}`, ); } const priorHash: Bytes = bundleBuilder.getPriorHash(); if ( priorHash.length != 32 || !sameData(oldChainInfo.hashCode, priorHash) ) { throw new Error("bad prior hash"); } } const chainInfo: [Medallion, ChainStart] = [ bundleInfo.medallion, bundleInfo.chainStart, ]; let verifyKey: Bytes = emptyBytes; const identity: string = bundleBuilder.getIdentity(); if (bundleInfo.timestamp === bundleInfo.chainStart) { ensure(identity, "chain start bundle missing identity"); this.identities.set(`${chainInfo[0]},${chainInfo[1]}`, identity); verifyKey = ensure(bundleBuilder.getVerifyKey()); this.verifyKeys.set(`${chainInfo[0]},${chainInfo[1]}`, verifyKey); } else { ensure(!identity, "non-chain start bundle has identity"); verifyKey = this.verifyKeys.get(`${chainInfo[0]},${chainInfo[1]}`); } verifyBundle(bundle.bytes, verifyKey); if (claimChain) { ensure(bundleInfo.timestamp === bundleInfo.chainStart); this.claimChain( bundleInfo.medallion, bundleInfo.chainStart, getActorId(), ); } this.chainInfos.set( medallionChainStartToString([medallion, chainStart]), bundleInfo, ); const bundleKey: BundleInfoTuple = MemoryStore.bundleInfoToKey(bundleInfo); this.trxns.set(bundleKey, bundle.bytes); // Decrypt bundle const encrypted = bundleBuilder.getEncrypted(); let changesList: Array<ChangeBuilder>; 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 = ensure( await this.getSymmetricKey(keyId), "could not find symmetric key referenced in bundle", ); const decrypted = decryptMessage(encrypted, symmetricKey); const innerBundleBuilder = <BundleBuilder>( 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]; ensure(offset > 0); const changeAddressTuple: MuidTuple = [ timestamp, medallion, offset, ]; const changeAddress = { timestamp, medallion, offset }; if (changeBuilder.hasContainer()) { const containerBytes = changeBuilder .getContainer() .serializeBinary(); this.containers.set( muidTupleToString(changeAddressTuple), containerBytes, ); continue; } if (changeBuilder.hasEntry()) { const entryBuilder: EntryBuilder = changeBuilder.getEntry(); let pointeeList = <Indexable[]>[]; if (entryBuilder.hasPointee()) { pointeeList = buildPointeeList(entryBuilder, bundleInfo); } let sourceList = <Indexable[]>[]; let targetList = <Indexable[]>[]; if (entryBuilder.hasPair()) { [sourceList, targetList] = buildPairLists( entryBuilder, bundleInfo, ); } const entry: Entry = { behavior: entryBuilder.getBehavior(), containerId: extractContainerMuid(entryBuilder, bundleInfo), storageKey: getStorageKey(entryBuilder, changeAddress), entryId: [timestamp, medallion, offset], pointeeList, value: entryBuilder.hasValue() ? unwrapValue(entryBuilder.getValue()) : undefined, expiry: entryBuilder.getExpiry() || undefined, deletion: entryBuilder.getDeletion(), placementId: [timestamp, medallion, offset], sourceList, targetList, purging: entryBuilder.getPurge(), }; this.addEntry(entry); continue; } if (changeBuilder.hasMovement()) { this.applyMovement( extractMovement(changeBuilder, bundleInfo, offset), ); continue; } if (changeBuilder.hasClearance()) { const clearanceBuilder = changeBuilder.getClearance(); const container = builderToMuid( clearanceBuilder.getContainer(), { timestamp, medallion, offset }, ); const containerMuidTuple: MuidTuple = [ container.timestamp, container.medallion, container.offset, ]; const containerIdStr = muidTupleToString(containerMuidTuple); if (clearanceBuilder.getPurge()) { // When purging, remove all entries from the container. while (true) { const it = this.placements.lowerBound(containerIdStr); const to = this.placements.upperBound( `${containerIdStr},~,~`, ); if (it.equals(to) || !it.key) break; this.placements.erase(it); this.byKeyPlacement.delete(it.key.slice(35)); // TODO: also delete removals, locations } // When doing a purging clear, remove previous clearances for the container. const lowerClearances = this.clearances.lowerBound( `${containerIdStr}`, ); const upperClearances = this.clearances.upperBound( `${containerIdStr},~`, ); while (lowerClearances) { if (lowerClearances.equals(upperClearances)) break; if ( muidTupleToString( lowerClearances.value.containerId, ) !== containerIdStr ) break; this.clearances.delete(lowerClearances.key); lowerClearances.next(); } } const clearance: Clearance = { containerId: containerMuidTuple, clearanceId: changeAddressTuple, purging: clearanceBuilder.getPurge(), }; // TODO: have entries check to see if there's a purging clearance when accepting an entry this.clearances.set( `${containerIdStr},${muidTupleToString(clearance.clearanceId)}`, clearance, ); continue; } throw new Error("don't know how to apply this kind of change"); } return true; } private static bundleInfoToKey(bundleInfo: BundleInfo): BundleInfoTuple { return [ bundleInfo.timestamp, bundleInfo.medallion, bundleInfo.chainStart, bundleInfo.priorTime || 0, bundleInfo.comment || "", ]; } applyMovement(movement: Movement) { const { entryId, movementId, dest, purge } = movement; const entryIdStr = muidTupleToString(entryId); const movementIdStr = muidTupleToString(movementId); let entry: Entry; if (purge || !this.keepingHistory) { const iterator = this.locations.lowerBound(entryIdStr); while (true) { if (iterator.equals(this.locations.end())) break; if (!iterator.key.startsWith(entryIdStr)) break; entry = { ...this.placements.get(iterator.value), placementId: movementId, storageKey: dest, }; this.placements.delete(iterator.value); this.byKeyPlacement.delete(iterator.value.slice(35)); this.locations.erase(iterator); iterator.next(); } } else { const iterator = toLastWithPrefixBeforeSuffix( this.locations, entryIdStr, ); if (!iterator) { console.error( `attempting to move something I don't have any record of: ${entryIdStr}`, ); return; } ensure(iterator.key && iterator.key.startsWith(entryIdStr)); entry = { ...this.placements.get(iterator.value), placementId: movementId, storageKey: dest, }; const removingIdStr = iterator.value.slice(-34); this.removals.set(`${removingIdStr},${movementIdStr}`, ""); } if (dest !== 0) { this.addEntry(entry); } } async getContainerBytes(address: Muid): Promise<Bytes | undefined> { return this.containers.get(muidToString(address)); } private asOfToTimestamp(asOf: AsOf): Timestamp { if (asOf instanceof Date) { return asOf.getTime() * 1000; } if (asOf > MemoryStore.YEAR_2020) { return asOf; } if (asOf < 0 && asOf > -1000) { // Interpret as number of bundles in the past. try { const trxnKeyArray = Array.from(this.trxns.keys()); const key = trxnKeyArray[trxnKeyArray.length + asOf]; return key[0]; } catch { // Looking further back than we have bundles. throw new Error("no bundles that far back"); } } throw new Error(`don't know how to interpret asOf=${asOf}`); } getEntryByKey( container?: Muid, key?: ScalarKey | Muid | [Muid, Muid], asOf?: AsOf, ): Promise<Entry | undefined> { try { return Promise.resolve( this.getEntryByKeyHelper(container, key, asOf), ); } catch (error) { return Promise.reject(error); } } getEntryByKeyHelper( container?: Muid, key?: ScalarKey | Muid | [Muid, Muid], asOf?: AsOf, ): Entry | undefined { const asOfTs = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const desiredSrc: [number, number, number] = [ container?.timestamp ?? 0, container?.medallion ?? 0, container?.offset ?? 0, ]; const srcAsStr = muidTupleToString(desiredSrc); let clearanceTime: Timestamp = this.getLastClearanceTime( srcAsStr, asOfTs, ); const semanticKey = toStorageKey(key); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const prefix = `${srcAsStr},${storageKeyToString(semanticKey)},`; const iterator = toLastWithPrefixBeforeSuffix( this.placements, prefix, asOfTsStr, ); if (!iterator) return undefined; const entry: Entry = iterator.value; if (!entry) throw new Error(`missing entry for: ${iterator.value}`); if (entry.placementId[0] < clearanceTime) { // container was cleared after this entry return undefined; } return entry; } async getBundles(callBack: (bundle: BundleView) => void) { for (const [_, val] of this.trxns) { callBack(new Decomposition(val)); } } getEntryById(entryMuid: Muid, asOf?: AsOf): Promise<Entry | undefined> { return Promise.resolve(this.getEntryByIdSync(entryMuid)); } // TODO: allow this to take an as-of timestamp getEntryByIdSync(entryMuid: Muid): Entry | undefined { const entryIdStr = muidToString(entryMuid); const it = toLastWithPrefixBeforeSuffix(this.locations, entryIdStr); if (!it) return undefined; return this.placements.get(it.value); } async getKeyedEntries( container: Muid, asOf?: AsOf, ): Promise<Map<string, Entry>> { const asOfTs = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const desiredSrc: [number, number, number] = [ container?.timestamp ?? 0, container?.medallion ?? 0, container?.offset ?? 0, ]; const srcAsStr = muidTupleToString(desiredSrc); const clearanceTime: Timestamp = this.getLastClearanceTime( srcAsStr, asOfTs, ); const clearTimeStr = muidTupleToString([clearanceTime, 0, 0]); const iterator = this.placements.lowerBound(srcAsStr); const result = new Map(); for ( ; iterator && iterator.key && !iterator.equals(this.placements.end()); iterator.next() ) { const parts = iterator.key.split(","); if (parts[0] !== srcAsStr) break; const placementIdStr = parts[parts.length - 1]; if (placementIdStr < clearTimeStr || placementIdStr > asOfTsStr) continue; const entry: Entry = iterator.value; ensure( entry.behavior === Behavior.DIRECTORY || entry.behavior === Behavior.KEY_SET || entry.behavior === Behavior.GROUP || entry.behavior === Behavior.PAIR_SET || entry.behavior === Behavior.PAIR_MAP || entry.behavior === Behavior.PROPERTY, ); const key = storageKeyToString(entry.storageKey); if (entry.deletion) result.delete(key); else result.set(key, entry); } return result; } async getContainersByName(name: string, asOf?: AsOf): Promise<Muid[]> { const asOfTs = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const desiredSrc: [number, number, number] = [ -1, -1, Behavior.PROPERTY, ]; const srcAsStr = muidTupleToString(desiredSrc); const clearanceTime: Timestamp = this.getLastClearanceTime( srcAsStr, asOfTs, ); const clearTimeStr = muidTupleToString([clearanceTime, 0, 0]); const iterator = this.byName.lowerBound(name); const result = []; for ( ; iterator && iterator.key && !iterator.equals(this.byName.end()); iterator.next() ) { const parts = iterator.key.split(","); if (parts[0] !== name) break; const entryMuidStr = parts[1]; if (entryMuidStr < clearTimeStr || entryMuidStr > asOfTsStr) continue; const describingMuid = strToMuid(iterator.value); result.push(describingMuid); } return result; } async getContainerProperties( containerMuid: Muid, asOf?: AsOf, ): Promise<Map<string, Value>> { const resultMap = new Map<string, Value>(); const asOfTs = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const strMuid = muidToString(containerMuid); const iterator = this.byKeyPlacement.lowerBound(strMuid); for ( ; iterator && iterator.key && !iterator.equals(this.byKeyPlacement.end()); iterator.next() ) { const parts = iterator.key.split(","); if (parts[0] !== strMuid) break; const entry = iterator.value; if (!(entry.behavior === Behavior.PROPERTY)) continue; const propertyMuid = muidTupleToString(entry.containerId); const clearTime = this.getLastClearanceTime(propertyMuid, asOfTs); const entryMuid = strToMuid(parts[1]); if ( entryMuid.timestamp < clearTime || entryMuid.timestamp > asOfTs ) { continue; } const containerMuid = entry.containerId; if (entry.deletion) { resultMap.delete(muidTupleToString(containerMuid)); } else { resultMap.set(muidTupleToString(containerMuid), entry.value); } } return resultMap; } /** * Returns entry data for a List. * @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: Muid, through = Infinity, asOf?: AsOf, ): Promise<Map<string, Entry>> { return Promise.resolve( this.getOrderedEntriesSync(container, through, asOf), ); } getOrderedEntriesSync( container: Muid, through = Infinity, asOf?: AsOf, ): Map<string, Entry> { const asOfTs: Timestamp = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const commaAsOfTsStr = "," + muidTupleToString([asOfTs, 0, 0]); const containerId: [number, number, number] = [ container?.timestamp ?? 0, container?.medallion ?? 0, container?.offset ?? 0, ]; const containerIdStr = muidTupleToString(containerId); let clearanceTime: Timestamp = this.getLastClearanceTime( containerIdStr, asOfTs, ); const clearanceTimeStr = muidTupleToString([clearanceTime, 0, 0]); // TODO: switch sequence key to be appropriately padded/encoded const lower = this.placements.lowerBound(`${containerIdStr}`); const upper = this.placements.upperBound(`${containerIdStr},${asOfTs}`); const returning = new Map<string, Entry>(); // If we need to iterate forward or backward let it: MapIterator<string, Entry>; if (through < 0) { it = upper; it.prev(); } else { it = lower; } const needed = through < 0 ? -through : through + 1; for (; returning.size < needed; through < 0 ? it.prev() : it.next()) { // '0616C86BB4D7B4-1A86FC813F72F-00001,1713899916549000,0616C86BB4DB88-1A86FC813F72F-00001' const placementKey = it.key; if (!placementKey) break; const foundContainerStr = placementKey.substring(0, 34); if (foundContainerStr !== containerIdStr) break; const placementIdStr = placementKey.slice(-34); if (placementIdStr < clearanceTimeStr || placementIdStr > asOfTsStr) continue; if ( toLastWithPrefixBeforeSuffix( this.removals, placementIdStr, commaAsOfTsStr, ) ) continue; const returningKey = placementKey.substring(35); const entry: Entry = it.value; ensure(muidTupleToString(entry.containerId) === containerIdStr); returning.set(returningKey, entry); } return returning; } getLocation(entry: Muid, asOf?: AsOf): Promise<Placement | undefined> { const asOfTs: Timestamp = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const prefix = `${muidToString(entry)}`; const suffix = `${muidToString(entry)},${asOfTsStr}`; const it = toLastWithPrefixBeforeSuffix(this.locations, prefix, suffix); if (!it) return undefined; const parts = it.value.split(","); const containerIdStr = parts[0]; const key = parts[1]; const placementIdStr = parts[2]; const placementTuple = strToMuidTuple(placementIdStr); const lastClear = this.getLastClearanceTime(containerIdStr, asOfTs); const removal = toLastWithPrefixBeforeSuffix( this.removals, placementIdStr, `${placementIdStr},${asOfTsStr}`, ); if (lastClear > placementTuple[0] || removal) return undefined; return Promise.resolve({ container: strToMuidTuple(containerIdStr), key: key, placement: placementTuple, }); } addEntry(entry: Entry) { const entryIdStr = muidTupleToString(entry.entryId); const containerIdStr = muidTupleToString(entry.containerId); const placementIdStr = muidTupleToString(entry.placementId); const placementKey = `${containerIdStr},${storageKeyToString(entry.storageKey)},${placementIdStr}`; const behavior = entry.behavior; if (behavior === Behavior.SEQUENCE || behavior === Behavior.EDGE_TYPE) { this.locations.set(`${entryIdStr},${placementIdStr}`, placementKey); } else { const containerIdStr = muidTupleToString(entry.containerId); const prefix = `${containerIdStr},${storageKeyToString(entry.storageKey)}`; for ( let iterator = toLastWithPrefixBeforeSuffix( this.placements, prefix, ); iterator && iterator.key && iterator.key.startsWith(prefix); iterator.prev() ) { if (entry.purging || !this.keepingHistory) { this.placements.erase(iterator); this.byKeyPlacement.delete(iterator.key.slice(35)); } else { const placementIdStr = iterator.key.slice(-34); this.removals.set(`${placementIdStr},${entryIdStr}`, ""); } } } this.placements.set(placementKey, entry); if (behavior == Behavior.ACCUMULATOR) { const delta = entry.value; if (typeof delta !== "bigint") throw new Error("Accumulator increment not an integer?"); let total = this.accumulatorTotals.get(containerIdStr) ?? BigInt(0); total = total + delta; this.accumulatorTotals.set(containerIdStr, total); return; } this.byKeyPlacement.set( `${storageKeyToString(entry.storageKey)},${placementIdStr}`, entry, ); if (entry.sourceList.length) { // TODO: remove these on deletion/purge const middle = behavior === Behavior.EDGE_TYPE ? storageKeyToString(entry.storageKey) : ""; const sourceIdStr = muidTupleToString(entry.sourceList[0]); this.bySource.set( `${sourceIdStr},${middle},${placementIdStr}`, entry, ); ensure(entry.targetList.length); const targetIdStr = muidTupleToString(entry.targetList[0]); this.byTarget.set( `${targetIdStr},${middle},${placementIdStr}`, entry, ); } if (entry.behavior == Behavior.PROPERTY && entry.containerId[0] == -1) { if ( Array.isArray(entry.storageKey) && entry.storageKey.length == 3 ) { let globalPropIterator = toLastWithPrefixBeforeSuffix( this.placements, containerIdStr, ); for ( let iterator = globalPropIterator; iterator && iterator.key && iterator.key.startsWith(containerIdStr); iterator.prev() ) { const foundEntry = iterator.value; if ( foundEntry.storageKey.toString() === entry.storageKey.toString() && foundEntry.value !== entry.value ) { this.byName.delete( foundEntry.value + "," + muidTupleToString(foundEntry.entryId), ); } } this.byName.set( entry.value + "," + muidTupleToString(entry.entryId), muidTupleToString(entry.storageKey), ); } else { throw new Error("global property storage key isnt a tuple?"); } } if (entry.behavior == Behavior.PROPERTY && entry.containerId[0] == -1) { if ( Array.isArray(entry.storageKey) && entry.storageKey.length == 3 ) { let globalPropIterator = toLastWithPrefixBeforeSuffix( this.placements, containerIdStr, ); for ( let iterator = globalPropIterator; iterator && iterator.key && iterator.key.startsWith(containerIdStr); iterator.prev() ) { const foundEntry = iterator.value; if ( foundEntry.storageKey.toString() === entry.storageKey.toString() && foundEntry.value !== entry.value ) { this.byName.delete( foundEntry.value + "," + muidTupleToString(foundEntry.entryId), ); } } this.byName.set( entry.value + "," + muidTupleToString(entry.entryId), muidTupleToString(entry.storageKey), ); } else { throw new Error("global property storage key isnt a tuple?"); } } } /** * Returns the timestamp of the last clearance for any given container. * @param containerId container muid as a string * @param asOf optional timestamp to query - finds the last clearance within the timeframe. * @returns the timestamp of the last clearance, or 0 if one wasn't found. */ getLastClearanceTime(containerId: string, asOf?: Timestamp): number { const asOfStr = asOf ? muidTupleToString([asOf, 0, 0]) : "~"; const upperClearance = this.clearances.upperBound( `${containerId},${asOfStr}`, ); upperClearance.prev(); let clearanceTime: number = 0; if ( upperClearance.value && sameData( containerId, muidTupleToString(upperClearance.value.containerId), ) ) { clearanceTime = upperClearance.value.clearanceId[0]; } return clearanceTime; } getEntriesBySourceOrTarget( vertex: Muid, source: boolean, asOf?: AsOf, ): Promise<Entry[]> { return Promise.resolve( this.getEntriesBySourceOrTargetSync(vertex, source, asOf), ); } getEntriesBySourceOrTargetSync( vertex: Muid, source: boolean, asOf?: AsOf, ): Entry[] { const asOfTs: Timestamp = asOf ? this.asOfToTimestamp(asOf) : generateTimestamp(); const vertexIdStr = muidToString(vertex); const map = source ? this.bySource : this.byTarget; const entries: Entry[] = []; for ( const it = map.lowerBound(vertexIdStr); it.key && it.key.startsWith(vertexIdStr); it.next() ) { const entry = it.value; if (this.isSoftDeleted(entry, asOfTs)) continue; if (entry.placementId[0] > asOfTs) continue; entries.push(it.value); } return entries; } getAllContainerTuples(): Promise<MuidTuple[]> { const arr = []; arr.push([-1, -1, 4], [-1, -1, 10]); for (const containerIdStr of this.containers.keys()) { arr.push(strToMuidTuple(containerIdStr)); } return Promise.resolve(arr); } isSoftDeleted(entry: Entry, asOfTs: Timestamp): boolean { const placementIdStr = muidTupleToString(entry.placementId); const asOfTsStr = muidTupleToString([asOfTs, 0, 0]); const upperBound = `${placementIdStr},${asOfTsStr}`; for ( const it = this.removals.lowerBound(placementIdStr); it.key && it.key < upperBound; it.next() ) { return true; } return false; } async close(): Promise<void> { await this.ready; delete this.trxns; delete this.chainInfos; delete this.activeChains; delete this.clearances; delete this.containers; delete this.removals; delete this.placements; return Promise.resolve(); } // for debugging, not part of the api/interface getAllEntryKeys(): Array<string> { return Array.from(this.placements.keys()); } // for debugging, not part of the api/interface getAllEntries(): Array<Entry> { return Array.from(this.placements.values()); } // for debugging, not part of the api/interface getAllRemovals(): TreeMap<string, string> { return this.removals; } addFoundBundleCallBack(callback: BroadcastFunc): void { this.foundBundleCallBacks.push(callback); } }