@x5e/gink
Version:
an eventually consistent database
282 lines (268 loc) • 11.3 kB
text/typescript
import { isEqual } from "lodash";
import { Database } from "./Database";
import { Container } from "./Container";
import { AsOf, EdgeData, Muid, Value, Bundler, Meta } from "./typedefs";
import { Behavior, ContainerBuilder } from "./builders";
import {
ensure,
entryToEdgeData,
muidToBuilder,
muidToString,
muidToTuple,
muidTupleToMuid,
muidTupleToString,
strToMuid,
wrapValue,
} from "./utils";
import { Edge } from "./Edge";
import { Vertex } from "./Vertex";
import { EntryBuilder } from "./builders";
import { ChangeBuilder } from "./builders";
import { PairBuilder } from "./builders";
import { construct } from "./factories";
import { Property } from "./Property";
import { movementHelper } from "./store_utils";
export class EdgeType extends Container {
private constructor(database: Database, address: Muid) {
super(database, address, Behavior.EDGE_TYPE);
}
static get(database?: Database, muid?: Muid): EdgeType {
database = database || Database.recent;
if (!muid) {
muid = { timestamp: -1, medallion: -1, offset: Behavior.EDGE_TYPE };
}
return new EdgeType(database, muid);
}
static async create(database?: Database, meta?: Meta): Promise<EdgeType> {
database = database || Database.recent;
const muid = await Container.addContainer({
behavior: Behavior.EDGE_TYPE,
database,
meta,
});
return new EdgeType(database, muid);
}
public size(): Promise<number> {
throw new Error("not implemented");
}
toJson(
indent: number | boolean,
asOf?: AsOf,
seen?: Set<string>,
): Promise<string> {
throw new Error("not implemented");
}
async create(
source: Vertex,
target: Vertex,
value?: Value,
meta?: Meta,
): Promise<Edge> {
const muid = await this.addEntry([source, target], value, meta);
const edgeData: EdgeData = {
source: source.address,
target: target.address,
etype: this.address,
value,
};
return Edge.get(this.database, muid, edgeData);
}
async reset(toTime?: AsOf, recurse?, meta?: Meta): Promise<void> {
if (recurse === true) {
recurse = new Set();
}
if (recurse instanceof Set) {
recurse.add(muidToString(this.address));
}
const bundler: Bundler = await this.database.startBundle(meta);
if (!toTime) {
// If no time is specified, we are resetting to epoch, which is just a clear
this.clear(false, { bundler });
} else {
const entriesThen = await this.database.store.getOrderedEntries(
this.address,
Infinity,
toTime,
);
const entriesNow = await this.database.store.getOrderedEntries(
this.address,
Infinity,
);
for (const [key, entry] of entriesThen) {
ensure(
isEqual(muidToTuple(this.address), entry.containerId),
"entry's containerId doesn't match this edgeType's address",
);
const edgeData = entryToEdgeData(entry);
const effectiveThen = entry.storageKey as number;
const placementNow = await this.database.store.getLocation(
muidTupleToMuid(entry.entryId),
);
const effectiveNow = placementNow
? (placementNow.key as number)
: undefined;
if (!effectiveNow) {
// This entry existed then, but has since been deleted
// Need to re-add it to the previous location
const entryBuilder = new EntryBuilder();
entryBuilder.setContainer(muidToBuilder(this.address));
/*
This confused me while writing this, so hopefully it helps
whoever is reading this:
The effective timestamp is set here, but it will also later
be the storageKey when the entry is converted from EntryBuilder
to Entry.
Setting the key here directly will get overwritten by setPair
since they are oneof in the proto definition.
*/
entryBuilder.setEffective(effectiveThen);
entryBuilder.setBehavior(entry.behavior);
if (entry.value) {
entryBuilder.setValue(wrapValue(entry.value));
}
const pairBuilder = new PairBuilder();
pairBuilder.setLeft(muidToBuilder(edgeData.source));
pairBuilder.setRite(muidToBuilder(edgeData.target));
entryBuilder.setPair(pairBuilder);
const changeBuilder = new ChangeBuilder();
changeBuilder.setEntry(entryBuilder);
const changeMuid = bundler.addChange(changeBuilder);
// Copy properties from the edge that previously existed
const propertiesThen =
await this.database.store.getContainerProperties(
muidTupleToMuid(entry.entryId),
toTime,
);
// Not using this.createEdge because we already added the
// entry to the bundler. this.createEdge does not allow
// us to adjust the position of the edge.
const newEdge = Edge.get(
this.database,
changeMuid,
edgeData,
);
for (const [key, value] of propertiesThen.entries()) {
const property = <Property>(
await construct(strToMuid(key), this.database)
);
ensure(
property.behavior === Behavior.PROPERTY,
"constructed container isn't a property?",
);
await property.set(newEdge, value, { bundler });
}
} else {
if (effectiveNow && effectiveNow !== effectiveThen) {
// This entry exists, but has been moved
// Need to move it back
await movementHelper(
bundler,
muidTupleToMuid(entry.entryId),
this.address,
effectiveThen,
false,
);
}
// reset the properties of this edge
await this.resetEdgeProperties(
muidTupleToMuid(entry.entryId),
edgeData,
toTime,
{ bundler },
);
// Need to remove the current entry from entriesNow if
// 1) the entry exists but was moved, or 2) the entry is untouched
ensure(
entriesNow.delete(
`${effectiveNow},${muidTupleToString(placementNow.placement)}`,
),
"entry not found in entriesNow",
);
}
}
// We will need to loop through the remaining entries in entriesNow
// to delete them, since we know they weren't active at toTime
for (const [key, entry] of entriesNow) {
await movementHelper(
bundler,
muidTupleToMuid(entry.entryId),
this.address,
undefined,
false,
);
}
}
if (!meta?.bundler) {
await bundler.commit();
}
}
/**
* Specific property reset method to reset the properties of an edge. Resets the properties
* associated with the edge to toTime.
*
* This is separated from the container reset method due to Edges being handled differently from
* containers (edges are not stored in the internal container database like other containers).
* @param edgeMuid the muid of the edge to reset
* @param edgeData the data of the edge to reset
* @param toTime optional timestamp to reset the properties to
* @param meta optionally may contain a bundler or comment
*/
private async resetEdgeProperties(
edgeMuid: Muid,
edgeData: EdgeData,
toTime?: AsOf,
meta?: Meta,
) {
let immediate = false;
const bundler: Bundler = await this.database.startBundle(meta);
const edge = Edge.get(this.database, edgeMuid, edgeData);
const propertiesNow = await this.database.store.getContainerProperties(
edge.address,
);
if (!toTime) {
// Resetting to epoch, so just delete all properties
for (const [key, _] of propertiesNow.entries()) {
const property = <Property>(
await construct(strToMuid(key), this.database)
);
ensure(
property.behavior === Behavior.PROPERTY,
"constructed container isn't a property?",
);
await property.delete(edge, { bundler });
}
} else {
const propertiesThen =
await this.database.store.getContainerProperties(edge, toTime);
for (const [key, value] of propertiesThen.entries()) {
if (value !== propertiesNow.get(key)) {
const property = <Property>(
await construct(strToMuid(key), this.database)
);
ensure(
property.behavior === Behavior.PROPERTY,
"constructed container isn't a property?",
);
await property.set(edge, value, { bundler });
}
// Remove from propertiesNow so we can delete the rest
// after this iteration
propertiesNow.delete(key);
}
// Now loop through the remaining entries in propertiesNow and delete them
for (const [key, _] of propertiesNow.entries()) {
const property = <Property>(
await construct(strToMuid(key), this.database)
);
ensure(
property.behavior === Behavior.PROPERTY,
"constructed container isn't a property?",
);
await property.delete(edge, { bundler });
}
}
if (immediate) {
await bundler.commit();
}
}
}