@x5e/gink
Version:
an eventually consistent database
147 lines • 6.34 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChainTracker = void 0;
const builders_1 = require("./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.
*/
class ChainTracker {
constructor({ greetingBytes = null, greeting = null }) {
this.data = new Map();
this.waiters = new Map();
if (greetingBytes) {
greeting = builders_1.GreetingBuilder.deserializeBinary(greetingBytes);
}
if (!greeting)
return;
for (const entry of greeting.getEntriesList()) {
const medallion = entry.getMedallion();
const chainStart = entry.getChainStart();
const timestamp = 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 }, timeoutMs) {
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 Metadata 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, checkValidExtension) {
var _a, _b;
if (!this.data.has(bundleInfo.medallion))
this.data.set(bundleInfo.medallion, new Map());
const innerMap = this.data.get(bundleInfo.medallion);
const seenThrough = ((_a = innerMap.get(bundleInfo.chainStart)) === null || _a === void 0 ? void 0 : _a.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 (((_b = bundleInfo.priorTime) !== null && _b !== void 0 ? _b : 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
*/
constructGreeting() {
const greeting = new builders_1.GreetingBuilder();
for (const [medallion, medallionMap] of this.data) {
for (const [chainStart, bundleInfo] of medallionMap) {
const entry = new builders_1.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() {
const greeting = this.constructGreeting();
const msg = new builders_1.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) {
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) {
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;
}
}
exports.ChainTracker = ChainTracker;
//# sourceMappingURL=ChainTracker.js.map