UNPKG

@x5e/gink

Version:

an eventually consistent database

302 lines 14.1 kB
"use strict"; 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