@x5e/gink
Version:
an eventually consistent database
302 lines • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sequence = void 0;
const Database_1 = require("./Database");
const Container_1 = require("./Container");
const utils_1 = require("./utils");
const factories_1 = require("./factories");
const builders_1 = require("./builders");
const builders_2 = require("./builders");
const store_utils_1 = require("./store_utils");
/**
* Kind of like the Gink version of a Javascript Array; supports push, pop, shift.
* Doesn't support unshift because order is defined by insertion order.
*/
class Sequence extends Container_1.Container {
constructor(database, address) {
super(database, address, builders_1.Behavior.SEQUENCE);
}
static get(database, muid) {
database = database || Database_1.Database.recent;
muid = muid ?? {
timestamp: -1,
medallion: -1,
offset: builders_1.Behavior.SEQUENCE,
};
return new Sequence(database, muid);
}
static async create(database, meta) {
database = database || Database_1.Database.recent;
const muid = await Container_1.Container.addContainer({
behavior: builders_1.Behavior.SEQUENCE,
database,
meta,
});
return new Sequence(database, muid);
}
/**
* Adds an element to the end of the list.
* @param value
* @param change change set to apply the change to or comment to put in
* @returns
*/
async push(value, meta) {
return await this.addEntry(undefined, value, meta);
}
async move(what, dest, purge, meta) {
let bundler = await this.database.startBundle(meta);
const store = this.database.store;
let muid;
if (typeof what === "number") {
muid = (0, utils_1.muidTupleToMuid)(Array.from((await store.getOrderedEntries(this.address, what)).values()).pop().entryId);
}
else {
muid = what;
}
(0, utils_1.ensure)(muid.timestamp && muid.medallion && muid.offset);
await (0, store_utils_1.movementHelper)(bundler, muid, this.address, await this.findDest(dest), purge);
if (!meta?.bundler) {
await bundler.commit();
}
}
async reset(toTime, recurse, meta) {
if (recurse === true)
recurse = new Set();
if (recurse instanceof Set) {
recurse.add((0, utils_1.muidToString)(this.address));
}
const 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);
// Need something subscriptable to compare by position
const entriesNow = await this.database.store.getOrderedEntries(this.address, Infinity);
for (const [key, entry] of entriesThen) {
const placementTupleThen = entry.placementId;
const placementNow = await this.database.store.getLocation((0, utils_1.muidTupleToMuid)(entry.entryId));
const placementTupleNow = placementNow
? placementNow.placement
: undefined;
if (!placementNow) {
// This entry existed then, but has since been deleted
// Need to re-add it to the previous location
const entryBuilder = new builders_2.EntryBuilder();
entryBuilder.setContainer((0, utils_1.muidToBuilder)(this.address));
entryBuilder.setKey((0, utils_1.wrapKey)(placementTupleThen[0]));
entryBuilder.setBehavior(entry.behavior);
if (entry.value !== undefined) {
entryBuilder.setValue((0, utils_1.wrapValue)(entry.value));
}
if (entry.pointeeList.length > 0) {
const pointeeMuid = (0, utils_1.muidTupleToMuid)(entry.pointeeList[0]);
entryBuilder.setPointee((0, utils_1.muidToBuilder)(pointeeMuid));
}
const changeBuilder = new builders_1.ChangeBuilder();
changeBuilder.setEntry(entryBuilder);
bundler.addChange(changeBuilder);
}
else {
if (placementTupleNow &&
placementTupleThen[0] !== placementTupleNow[0]) {
// This entry exists, but has been moved
// Need to move it back
await (0, store_utils_1.movementHelper)(bundler, (0, utils_1.muidTupleToMuid)(entry.entryId), this.address, placementTupleThen[0], false);
}
// Need to remove the current entry from entriesNow if
// 1) the entry exists but was moved, or 2) the entry is untouched
(0, utils_1.ensure)(entriesNow.delete(`${placementTupleNow[0]},${(0, utils_1.muidTupleToString)(entry.entryId)}`), "entry not found in entriesNow");
}
// Finally, if the previous entry was a container, recusively reset it
if (recurse && entry.pointeeList.length > 0) {
const pointeeMuid = (0, utils_1.muidTupleToMuid)(entry.pointeeList[0]);
if (!recurse.has((0, utils_1.muidToString)(pointeeMuid))) {
const container = await (0, factories_1.construct)(pointeeMuid, this.database);
await container.reset(toTime, recurse, { bundler });
}
}
}
// We will need to loop through the remaining entries in entriesNow
// to delete them, since we know they weren't in the sequence at toTime
for (const [key, entry] of entriesNow) {
await (0, store_utils_1.movementHelper)(bundler, (0, utils_1.muidTupleToMuid)(entry.entryId), this.address, undefined, false);
}
}
if (!meta?.bundler) {
await bundler.commit();
}
}
async findDest(dest) {
if (dest === 0 || dest === -1) {
const currentFrontOrBack = ((await this.getEntryAt(dest)).storageKey);
return (currentFrontOrBack -
Math.sign(dest + 0.5) * Math.floor(1e3 * Math.random()));
}
if (dest > +1e6)
return dest;
if (dest < -1e6)
return (0, utils_1.generateTimestamp)() + dest;
const entryMap = await this.database.store.getOrderedEntries(this.address, dest);
const entryArray = Array.from(entryMap.entries());
const a = entryArray[entryArray.length - 2];
const b = entryArray[entryArray.length - 1];
const aTs = Number.parseInt(a[0].split(",")[0]);
const bTs = Number.parseInt(b[0].split(",")[0]);
if (Math.abs(aTs - bTs) < 2)
throw new Error("can't find space between entries");
return Math.floor((aTs + bTs) / 2);
}
async pop(what, purge, meta) {
let bundler = await this.database.startBundle(meta);
let returning;
let muid;
if (what && typeof what != "number" && what.offset) {
muid = what;
const entry = await this.database.store.getEntryById(muid);
if (!entry)
return undefined;
(0, utils_1.ensure)(entry.entryId[0] === muid.timestamp &&
entry.entryId[2] === muid.offset);
returning = await (0, factories_1.interpret)(entry, this.database);
}
else {
const position = what === undefined ? -1 : what;
// Should probably change the implementation to not copy all intermediate entries into memory.
const entries = Array.from((await this.database.store.getOrderedEntries(this.address, position)).values());
if (entries.length === 0)
return undefined;
const entry = entries[entries.length - 1];
returning = await (0, factories_1.interpret)(entry, this.database);
muid = (0, utils_1.muidTupleToMuid)(entry.entryId);
}
await (0, store_utils_1.movementHelper)(bundler, muid, this.address, undefined, purge);
if (!meta?.bundler) {
await bundler.commit();
}
return returning;
}
/**
* Alias for this.pop with position of 0
*/
async shift(purge, meta) {
return await this.pop(0, purge, meta);
}
/**
* Adds multiple entries into this sequence.
* NOTE: If you pass a bundler, all changes will share the same timestamp. This means you will
* not be able to move new entries in between these (you may move these entries between one another).
* Without a bundler, each item from the iterable will be committed separately, which will be costly,
* but there won't be the same restrictions on moving.
* @param iterable An iterable of stuff to add to the sequence.
* @param meta optional place to pass in a comment or bundler
*/
async extend(iterable, meta) {
const bundler = await this.database.startBundle(meta);
for (const value of iterable) {
await this.push(value, { bundler });
}
if (!meta?.bundler)
await bundler.commit();
}
async getEntryAt(position, asOf) {
//TODO add a store method to only return the entry at a given location
const entries = await this.database.store.getOrderedEntries(this.address, position, asOf);
if (entries.size === 0)
return undefined;
if (position >= 0 && position >= entries.size)
return undefined;
if (position < 0 && Math.abs(position) > entries.size)
return undefined;
let val;
for (let found of entries.values()) {
val = found;
}
return val;
}
/**
*
* @param position Index to look for the thing, negative counts from end, or muid of entry
* @param asOf
* @returns value at the position of the list, or undefined if list is too small
*/
async at(position, asOf) {
if (typeof position === "number") {
const entry = await this.getEntryAt(position, asOf);
return await (0, factories_1.interpret)(entry, this.database);
}
throw Error("unexpected");
}
/**
* Dumps the contents of this list to a javascript array.
* useful for debugging and could also be used to export data by walking the tree
* @param through how many elements to get, positive starting from beginning, negative starting from end
* @param asOf effective time to get the dump for: leave undefined to get data as of the present
* @returns an array containing Values (e.g. numbers, strings) and Containers (e.g. other Lists, Boxes, Directories)
*/
async toArray(through = Infinity, asOf) {
const thisList = this;
const entries = await thisList.database.store.getOrderedEntries(thisList.address, through, asOf);
const applied = Array.from(entries.values());
return await Promise.all(applied.map(async function (entry) {
return await (0, factories_1.interpret)(entry, thisList.database);
}));
}
async size(asOf) {
const entries = await this.database.store.getOrderedEntries(this.address, Infinity, asOf);
return entries.size;
}
/**
* Function to iterate over the contents of the List, showing the address of each entry (which can be used in pop).
* @param through count of many things to iterate through, positive starting from front, negative for end
* @param asOf effective time to get the contents for
* @returns an async iterator across everything in the list, with values returned being pairs of Muid, (Value|Container),
*/
entries(through = Infinity, asOf) {
const thisList = this;
return (async function* () {
// Note: I'm loading all entries memory despite using an async generator due to shitty IndexedDb
// behavior of closing transactions when you await on something else. Hopefully they'll fix that in
// the future and I can improve this. Alternative, it might make sense to hydrate everything in a single pass.
const entries = await thisList.database.store.getOrderedEntries(thisList.address, through, asOf);
for (const entry of entries) {
const hydrated = await (0, factories_1.interpret)(entry[1], thisList.database);
yield [(0, utils_1.muidTupleToMuid)(entry[1].entryId), hydrated];
}
})();
}
/**
* Generates a JSON representation of the data in the list.
* Mostly intended for demo/debug purposes.
* @param indent true to pretty print
* @param asOf effective time
* @param seen (internal use only! This prevents cycles from breaking things)
* @returns a JSON string
*/
async toJson(indent = false, asOf, seen) {
if (seen === undefined)
seen = new Set();
(0, utils_1.ensure)(indent === false, "indent not implemented");
const mySig = (0, utils_1.muidToString)(this.address);
if (seen.has(mySig))
return "null";
seen.add(mySig);
const asArray = await this.toArray(Infinity, asOf);
let returning = "[";
let first = true;
for (const value of asArray) {
if (first) {
first = false;
}
else {
returning += ",";
}
returning += await (0, factories_1.toJson)(value, indent === false ? false : +indent + 1, asOf, seen);
}
returning += "]";
return returning;
}
}
exports.Sequence = Sequence;
//# sourceMappingURL=Sequence.js.map