@x5e/gink
Version:
an eventually consistent database
344 lines • 16.9 kB
JavaScript
;
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sequence = void 0;
const Container_1 = require("./Container");
const Bundler_1 = require("./Bundler");
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, containerBuilder) {
super(database, address, builders_1.Behavior.SEQUENCE);
if (this.address.timestamp < 0) {
//TODO(https://github.com/google/gink/issues/64): document default magic containers
(0, utils_1.ensure)(address.offset === builders_1.Behavior.SEQUENCE, "magic tag not SEQUENCE");
}
else {
(0, utils_1.ensure)(containerBuilder.getBehavior() === builders_1.Behavior.SEQUENCE, "container not sequence");
}
}
/**
* 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, change) {
return await this.addEntry(undefined, value, change);
}
async move(muidOrPosition, dest, purge, bundlerOrComment) {
let immediate = false;
let bundler;
if (bundlerOrComment instanceof Bundler_1.Bundler) {
bundler = bundlerOrComment;
}
else {
immediate = true;
bundler = new Bundler_1.Bundler(bundlerOrComment);
}
const store = this.database.store;
// TODO: clarify what's going on here
const muid = typeof muidOrPosition === "object"
? muidOrPosition
: (0, utils_1.muidTupleToMuid)(Array.from((await store.getOrderedEntries(this.address, muidOrPosition)).values()).pop().entryId);
(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 (immediate) {
await this.database.addBundler(bundler);
}
}
async reset(args) {
var _a;
const toTime = args === null || args === void 0 ? void 0 : args.toTime;
const bundlerOrComment = args === null || args === void 0 ? void 0 : args.bundlerOrComment;
const skipProperties = args === null || args === void 0 ? void 0 : args.skipProperties;
const recurse = args === null || args === void 0 ? void 0 : args.recurse;
const seen = recurse ? ((_a = args === null || args === void 0 ? void 0 : args.seen) !== null && _a !== void 0 ? _a : new Set()) : undefined;
if (seen) {
seen.add((0, utils_1.muidToString)(this.address));
}
let immediate = false;
let bundler;
if (bundlerOrComment instanceof Bundler_1.Bundler) {
bundler = bundlerOrComment;
}
else {
immediate = true;
bundler = new Bundler_1.Bundler(bundlerOrComment);
}
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 (seen && entry.pointeeList.length > 0) {
const pointeeMuid = (0, utils_1.muidTupleToMuid)(entry.pointeeList[0]);
if (!seen.has((0, utils_1.muidToString)(pointeeMuid))) {
const container = await (0, factories_1.construct)(this.database, pointeeMuid);
await container.reset({
toTime,
bundlerOrComment: bundler,
skipProperties,
recurse,
seen,
});
}
}
}
// 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 (!skipProperties) {
await this.resetProperties(toTime, bundler);
}
if (immediate) {
await this.database.addBundler(bundler);
}
}
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);
}
/**
* Removes and returns the specified entry of the list (default last),
* in the provided change set or immediately if no CS is supplied.
* Returns undefined when called on an empty list (and no changes are made).
* @param what - position or Muid, defaults to last
* @param purge - If true, removes so data cannot be recovered with "asOf" query
* @param bundlerOrComment
*/
async pop(what, purge, bundlerOrComment) {
let immediate = false;
let bundler;
if (bundlerOrComment instanceof Bundler_1.Bundler) {
bundler = bundlerOrComment;
}
else {
immediate = true;
bundler = new Bundler_1.Bundler(bundlerOrComment);
}
let returning;
let muid;
if (what && typeof what === "object") {
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 {
what = typeof what === "number" ? what : -1;
// Should probably change the implementation to not copy all intermediate entries into memory.
const entries = Array.from((await this.database.store.getOrderedEntries(this.address, what)).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 (immediate) {
await this.database.addBundler(bundler);
}
return returning;
}
/**
* Alias for this.pop(0, purge, bundlerOrComment)
*/
async shift(purge, bundlerOrComment) {
return await this.pop(0, purge, bundlerOrComment);
}
/**
* 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 bundlerOrComment A bundler or comment for these changes
*/
async extend(iterable, bundlerOrComment) {
for (const value of iterable) {
await this.push(value, bundlerOrComment);
}
}
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 (function () {
return __asyncGenerator(this, arguments, 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 = yield __await(thisList.database.store.getOrderedEntries(thisList.address, through, asOf));
for (const entry of entries) {
const hydrated = yield __await((0, factories_1.interpret)(entry[1], thisList.database));
yield yield __await([(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