@x5e/gink
Version:
an eventually consistent database
1,026 lines (968 loc) • 38.3 kB
text/typescript
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);
}
}