@x5e/gink
Version:
an eventually consistent database
232 lines (205 loc) • 7.04 kB
text/typescript
import {
BundleInfo,
BundleView,
ChainStart,
Connection,
Medallion,
Timestamp,
} from "./typedefs";
import { HasMap } from "./HasMap";
import {
AckBuilder,
SyncMessageBuilder,
Signal,
} from "./builders";
export class AbstractConnection implements Connection {
protected listeners: Array<() => void> = [];
protected peerHasMap?: HasMap; // Data the peer has said that it has or we have sent it.
private unacked: Map<Medallion, Map<ChainStart, Timestamp>> = new Map();
private unackedChains: number = 0;
private hasSentInitialSyncState: boolean = false;
private hasRecvInitialSyncState: boolean = false;
private hasSentGreetingState: boolean = false;
private isReadOnlyState: boolean = false;
private _ready: Promise<void>;
private onReady: (() => void) | undefined;
constructor() {
this.resetAbstractConnection();
}
get ready(): Promise<void> {
return this._ready;
}
protected resetAbstractConnection() {
this.unacked = new Map();
this.unackedChains = 0;
this.peerHasMap = undefined;
this.hasSentInitialSyncState = false;
this.hasRecvInitialSyncState = false;
this.hasSentGreetingState = false;
this.peerHasMap = undefined;
this._ready = new Promise((resolve) => {
this.onReady = resolve;
});
}
get synced(): boolean {
return (
this.hasSentGreeting &&
(this.hasSentInitialSync || this.isReadOnly) &&
this.hasRecvInitialSync &&
this.connected &&
!this.hasSentUnackedData
);
}
get connected(): boolean {
throw new Error("Not implemented");
}
get isReadOnly(): boolean {
return this.isReadOnlyState;
}
set isReadOnly(value: boolean) {
this.isReadOnlyState = value;
}
get hasSentGreeting(): boolean {
return this.hasSentGreetingState;
}
get hasSentInitialSync(): boolean {
return this.hasSentInitialSyncState;
}
get hasRecvInitialSync(): boolean {
return this.hasRecvInitialSyncState;
}
markHasSentGreeting() {
this.hasSentGreetingState = true;
this.notify();
}
markHasSentInitialSync() {
this.hasSentInitialSyncState = true;
this.notify();
}
markHasRecvInitialSync() {
this.hasRecvInitialSyncState = true;
this.notify();
}
get hasSentUnackedData(): boolean {
return this.unackedChains > 0;
}
onAck(bundleInfo: BundleInfo) {
const innerMap = this.unacked.get(bundleInfo.medallion);
if (!innerMap) {
console.error(
"Received an ack for a medallion we don't have?",
bundleInfo,
);
return;
}
const lastSentForThisChain: Timestamp | undefined = innerMap.get(
bundleInfo.chainStart,
);
if (!lastSentForThisChain) {
console.error(
"received an ack for a chain we didn't send?",
bundleInfo,
);
return;
}
if (bundleInfo.timestamp === lastSentForThisChain) {
innerMap.delete(bundleInfo.chainStart);
if (this.unackedChains > 0) {
this.unackedChains--;
if (this.unackedChains === 0) {
this.notify();
}
} else {
console.error("expected unacked chains to be > 0");
}
}
}
protected notify() {
this.listeners.forEach((listener) => listener());
if (this.synced) {
this.onReady?.();
}
}
subscribe(callback: () => void): () => void {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(
(listener) => listener !== callback,
);
};
}
send(_: Uint8Array) {
throw new Error("Not implemented");
}
sendInitialBundlesSent() {
const message = new SyncMessageBuilder();
const signal = Signal.INITIAL_BUNDLES_SENT;
message.setSignal(signal);
const bundleBytes = message.serializeBinary();
this.send(bundleBytes);
}
close() {
throw new Error("Not implemented");
}
setPeerHasMap(hasMap?: HasMap) {
if (this.peerHasMap && hasMap) {
throw new Error(
"Already received a HasMap/Greeting from this Peer?",
);
}
this.peerHasMap = hasMap;
}
/**
* The Message proto contains an embedded one-of. Essentially this will wrap
* the bundle bytes payload in a wrapper by prefixing a few bytes to it.
* In theory the "Message" proto could be expanded with some extra meta
* (e.g. send time) in the future.
* Note that the bundle is always passed around as bytes and then
* parsed as needed to avoid losing unknown fields.
* @param bundleBytes: the bytes corresponding to a bundle
* @returns a serialized "Message" proto
*/
private static makeBundleMessage(bundleBytes: Uint8Array): Uint8Array {
const message = new SyncMessageBuilder();
message.setBundle(bundleBytes);
return message.serializeBinary();
}
/**
* Sends a bundle if we've received a greeting and our internal recordkeeping indicates
* that the peer could use this particular bundle (but ensures that we're not sending
* bundles that would cause gaps in the peer's chain.)
* @param bundleBytes The bundle to be sent.
* @param bundleInfo Meta about the bundle.
*/
sendIfNeeded(bundle: BundleView) {
if (this.peerHasMap?.markAsHaving(bundle.info, true)) {
this.send(AbstractConnection.makeBundleMessage(bundle.bytes));
if (!this.unacked.has(bundle.info.medallion)) {
this.unacked.set(bundle.info.medallion, new Map());
}
const innerMap = this.unacked.get(bundle.info.medallion);
const hadUnacked = this.unackedChains > 0;
if (!innerMap.has(bundle.info.chainStart)) {
this.unackedChains++;
}
innerMap.set(bundle.info.chainStart, bundle.info.timestamp);
if (!hadUnacked) {
this.notify();
}
}
}
onReceivedBundle(bundleInfo: BundleInfo) {
this.peerHasMap?.markAsHaving(bundleInfo);
this.sendAck(bundleInfo);
}
private sendAck(changeSetInfo: BundleInfo) {
const ack = new AckBuilder();
ack.setMedallion(changeSetInfo.medallion);
ack.setChainStart(changeSetInfo.chainStart);
ack.setTimestamp(changeSetInfo.timestamp);
const syncMessageBuilder = new SyncMessageBuilder();
syncMessageBuilder.setAck(ack);
const bytes = syncMessageBuilder.serializeBinary();
this.send(bytes);
}
}