@x5e/gink
Version:
an eventually consistent database
176 lines (167 loc) • 6.75 kB
text/typescript
import {
BundleInfo,
Medallion,
ChainStart,
SeenThrough,
Muid,
CallBack,
Timestamp,
} from "./typedefs";
import {
SyncMessageBuilder,
GreetingBuilder,
GreetingEntryBuilder,
} from "./builders";
/**
* A class to keep track of what data a given instance (self or peer) has for each
* chain. So it's kind of like Map<[Medallion, ChainStart], SeenThrough>.
* This is essentially the same data that's in the Greeting message, so I've included
* functionality to convert from/to Greeting objects.
*/
export class HasMap {
private readonly data: Map<Medallion, Map<ChainStart, BundleInfo>> =
new Map();
private readonly waiters: Map<CallBack, [Medallion, Timestamp]> = new Map();
constructor({ greetingBytes = null, greeting = null }) {
if (greetingBytes) {
greeting = GreetingBuilder.deserializeBinary(greetingBytes);
}
if (!greeting) return;
for (const entry of greeting.getEntriesList()) {
const medallion: Medallion = entry.getMedallion();
const chainStart: ChainStart = entry.getChainStart();
const timestamp: SeenThrough = entry.getSeenThrough();
if (!this.data.has(medallion)) {
this.data.set(medallion, new Map());
}
this.data
.get(medallion)
.set(chainStart, { medallion, chainStart, timestamp });
}
}
/**
* Allows you to wait until an instance has seen a particular bundle.
* @param what either a muid address or a bundle info (indicates what to watch for)
* @param timeoutMs how long to wait before giving up, default of undefined doesn't time out
* @returns a promise that resolves when the thing has been marked as seen, or rejects at timeout
*/
waitTillHas(
{ medallion, timestamp }: BundleInfo | Muid,
timeoutMs?: number,
): Promise<void> {
const innerMap = this.data.get(medallion);
if (innerMap) {
for (const [chainStart, bundleInfo] of innerMap.entries()) {
if (
chainStart <= timestamp &&
bundleInfo.timestamp >= timestamp
)
return Promise.resolve();
}
}
const waiters = this.waiters;
//TODO: prune waiters after their timeout
return new Promise((resolve, reject) => {
if (timeoutMs) setTimeout(reject, timeoutMs);
waiters.set(resolve, [medallion, timestamp]);
});
}
/**
* First, determine if the bundle is novel (represents data not previously marked),
* then second, mark the data in the data structure (possibly checking that it's a sensible extension).
* Note that checkValidExtension is used here as a safeguard to make sure we don't
* send broken chains to the peer; the store should have its own check for receiving.
* @param bundleInfo Meta about a particular bundle.
* @param checkValidExtension If true then barfs if this bundle isn't a valid extension.
* @returns true if the bundle represents data not seen before
*/
markAsHaving(
bundleInfo: BundleInfo,
checkValidExtension?: boolean,
): boolean {
if (!this.data.has(bundleInfo.medallion))
this.data.set(bundleInfo.medallion, new Map());
const innerMap = this.data.get(bundleInfo.medallion);
const seenThrough = innerMap.get(bundleInfo.chainStart)?.timestamp || 0;
if (bundleInfo.timestamp > seenThrough) {
if (checkValidExtension) {
if (
bundleInfo.timestamp !== bundleInfo.chainStart &&
!bundleInfo.priorTime
)
throw new Error(
`bundleInfo appears to be invalid: ${JSON.stringify(bundleInfo)}`,
);
if ((bundleInfo.priorTime ?? 0) !== seenThrough)
throw new Error(
`proposed bundle would be an invalid extension ${JSON.stringify(bundleInfo)}`,
);
}
innerMap.set(bundleInfo.chainStart, bundleInfo);
for (const [cb, pair] of this.waiters) {
if (
pair[0] === bundleInfo.medallion &&
pair[1] >= bundleInfo.chainStart &&
pair[1] <= bundleInfo.timestamp
) {
this.waiters.delete(cb);
cb();
}
}
return true;
}
return false;
}
/**
* Constructs the greeting for use during the initial handshake. Note that
* the priorTimes aren't included, so recipient should not markIfNovel using
* @returns
*/
private constructGreeting(): GreetingBuilder {
const greeting = new GreetingBuilder();
for (const [medallion, medallionMap] of this.data) {
for (const [chainStart, bundleInfo] of medallionMap) {
const entry = new GreetingEntryBuilder();
entry.setMedallion(medallion);
entry.setChainStart(chainStart);
entry.setSeenThrough(bundleInfo.timestamp);
greeting.addEntries(entry);
}
}
return greeting;
}
/**
* @returns bytes that can be sent during the initial handshake
*/
getGreetingMessageBytes(): Uint8Array {
const greeting = this.constructGreeting();
const msg = new SyncMessageBuilder();
msg.setGreeting(greeting);
return msg.serializeBinary();
}
/**
* Returns how far along data is seen for a particular chain.
* @param key A [Medallion, ChainStart] tuple
* @returns SeenThrough (a Timestamp) or undefined if not yet seen
*/
getBundleInfo(key: [Medallion, ChainStart]): BundleInfo | undefined {
const inner = this.data.get(key[0]);
if (!inner) return undefined;
return inner.get(key[1]);
}
/**
* Gets a list of chains seen for a particular medallion, or a list of all seen chains
* @param singleMedallion The single medallion to get chains for (returns all if undefined)
* @returns a list of known chains
*/
getChains(singleMedallion?: Medallion): Array<[Medallion, ChainStart]> {
const result = [];
for (const [medallion, map] of this.data.entries()) {
if (singleMedallion && medallion !== singleMedallion) continue;
for (const chainStart of map.keys()) {
result.push([medallion, chainStart]);
}
}
return result;
}
}