@e280/quay
Version:
File-browser and outliner UI for the web
116 lines • 4.16 kB
JavaScript
import { collect } from "@e280/kv";
/**
* Async hierarchy of nestable items.
* - items can have multiple children.
* - items can only have one parent.
* - items can be orphans (bring your own concept of a "root").
*/
export class HyTree {
records;
constructor(records) {
this.records = records;
}
async has(id) {
return this.records.has(id);
}
async get(id) {
const record = await this.records.get(id);
if (record) {
const [parent, children] = record;
return { parent, children };
}
}
async gets(...ids) {
const records = await this.records.gets(...ids);
return records.map(r => {
if (!r)
return undefined;
const [parent, children] = r;
return { parent, children };
});
}
async require(id) {
const record = await this.records.require(id);
const [parent, children] = record;
return { parent, children };
}
async requires(...ids) {
const records = await this.records.requires(...ids);
return records.map(([parent, children]) => ({ parent, children }));
}
/** establish an id as a root with no parents, but ready to accept children */
async establishRoot(root) {
return this.records.set(root, [null, []]);
}
/** give a parent some children */
async attach(parent, ...children) {
children = [...new Set(children)];
const parentItem = await this.require(parent);
const siblings = new Set(parentItem.children);
const childItems = await this.gets(...children);
const newcomers = [];
for (const [index, id] of children.entries()) {
const item = childItems.at(index);
// child already in tree
if (item && item.parent)
throw new Error(`child already has parent`);
// child not in tree yet, remember to save it
else
newcomers.push([id, [parent, []]]);
// add to siblings set
siblings.add(id);
}
// save all new children
await this.records.sets(...newcomers);
// save updated parent item
await this.records.set(parent, [parentItem.parent, [...siblings]]);
}
/** detach this id from its parent in the hierarchy */
async detach(id) {
const item = await this.get(id);
if (!item)
return undefined;
if (item.parent) {
const parentItem = await this.require(item.parent);
const siblings = new Set(parentItem.children);
siblings.delete(id);
// save the parent item with removed child
await this.records.set(item.parent, [parentItem.parent, [...siblings]]);
// save the child with null parent
await this.records.set(id, [null, item.children]);
}
}
/** destroy all relations associated with this id, and all its descendants */
async destroy(id) {
await this.detach(id);
const tree = await collect(this.crawl(id));
const ids = tree.map(({ id }) => id);
await this.records.del(...ids);
}
/** delete absolutely everything */
async clear() {
await this.records.clear();
}
/** iterate over all root items */
async *roots() {
for await (const [id, [parent, children]] of this.records.entries())
if (parent === null)
yield { id, item: { parent, children } };
}
/** iterate over this id and all its descendants */
async *crawl(id, predicate = async () => true) {
const todo = [[id, []]];
const seen = new Set();
while (todo.length) {
const [id, path] = todo.shift();
if (seen.has(id) || !await predicate(id, path))
continue;
seen.add(id);
const item = await this.require(id);
yield { id, item, path };
for (const childId of item.children)
todo.push([childId, [...path, id]]);
}
}
}
//# sourceMappingURL=hy-tree.js.map