@loken/hierarchies
Version:
Library for working with hierarchies of identifiers and identifiable objects.
267 lines (266 loc) • 11.3 kB
JavaScript
var __typeError = (msg) => {
throw TypeError(msg);
};
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value), __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value), __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
import { someToArray, isSomeItem } from "@loken/utilities";
import { traverseFullGraph } from "../traversal/graph-traverse.js";
import { traverseSequence } from "../traversal/sequence-traverse.js";
import { nodesToItems } from "./node-conversion.js";
import { flattenGraph } from "../traversal/graph-flatten.js";
import { flattenSequence } from "../traversal/sequence-flatten.js";
import { searchSequence, searchSequenceMany } from "../traversal/sequence-search.js";
import { searchGraph, searchGraphMany } from "../traversal/graph-search.js";
var _item, _parent, _children, _brand, _HCNode_instances, getRoots_fn;
class HCNode {
/** Create a node wrapping the `item`. */
constructor(item) {
__privateAdd(this, _HCNode_instances);
//#region backing fields
__privateAdd(this, _item);
__privateAdd(this, _parent);
__privateAdd(this, _children);
/** The brand is used to lock the node to a specific owner. */
__privateAdd(this, _brand);
__privateSet(this, _item, item);
}
//#endregion
//#region predicates
/** A node is a "root" when there is no `parent`. */
get isRoot() {
return __privateGet(this, _parent) === void 0;
}
/** A node is a "leaf" when there are no `children`. */
get isLeaf() {
return __privateGet(this, _children) === void 0 || __privateGet(this, _children).size === 0;
}
/** A node is "internal" when it has `children`, meaning it's either "internal" or a "leaf". */
get isInternal() {
return !this.isLeaf;
}
/**
* A node is "linked" when it is neither a root nor a child.
*
* A node is "linked" when it has a parent or at least one child.
*/
get isLinked() {
return !this.isRoot || !this.isLeaf;
}
/** Having a brand means that some other entity "owns" the node. */
get isBranded() {
return __privateGet(this, _brand) !== void 0;
}
//#endregion
//#region brands
/**
* When the brands of two nodes are compatible, they may be linked/attached
* in a parent-child relationship.
*/
isBrandCompatible(other) {
return __privateGet(this, _brand) === void 0 ? __privateGet(other, _brand) === void 0 : __privateGet(other, _brand) === void 0 ? !1 : __privateGet(this, _brand) === __privateGet(other, _brand);
}
/**
* Adds the provided `brand` to the node,
* providing an `DeBrand` delegate for removing/clearing the brand.
*/
brand(brand) {
if (brand === void 0)
throw new Error("The brand cannot be 'undefined'.");
if (__privateGet(this, _brand) !== void 0)
throw new Error("Must clear existing brand using the 'DeBrand' delegate before you can re-brand a node.");
return __privateSet(this, _brand, brand), () => __privateSet(this, _brand, void 0);
}
//#endregion
//#region links
/**
* Attach the provided `children`.
*/
attach(children) {
const nodes = someToArray(children);
if (nodes.length === 0)
throw new Error("Must provide one or more 'children'.");
if (!nodes.every((node) => node.isRoot))
throw new Error("Must all be without a 'parent' before attaching to another.");
if (!nodes.every((node) => node.isBrandCompatible(this)))
throw new Error("Must all have a compatible brand.");
__privateGet(this, _children) ?? __privateSet(this, _children, /* @__PURE__ */ new Set());
for (const child of nodes)
__privateGet(this, _children).add(child), __privateSet(child, _parent, this);
return this;
}
/** Detach the provided `children`. */
detach(children) {
const nodes = someToArray(children);
if (nodes.length === 0)
throw new Error("Must provide one or more 'children'.");
if (this.isLeaf || !nodes.every((child) => __privateGet(this, _children).has(child)))
throw new Error("Must all be 'children'.");
if (nodes.some((child) => child.isBranded))
throw new Error("Must clear brand using the 'DeBrand' delegate before you can detach a branded node.");
for (const node of nodes)
__privateGet(this, _children).delete(node), __privateSet(node, _parent, void 0);
return this.isLeaf && __privateSet(this, _children, void 0), this;
}
/** Detach the node from it's `parent`. */
detachSelf() {
if (this.isRoot)
throw new Error("Can't detach a root node as there's nothing to detach it from.");
if (this.isBranded)
throw new Error("Must clear brand using the 'DeBrand' delegate before you can detach a branded node.");
if (__privateGet(this, _parent).isLeaf || !__privateGet(__privateGet(this, _parent), _children).has(this))
throw new Error("Invalid object state: It should not be possible for the node not to be a child of its parent!.");
return __privateGet(__privateGet(this, _parent), _children).delete(this), __privateGet(this, _parent).isLeaf && __privateSet(__privateGet(this, _parent), _children, void 0), __privateSet(this, _parent, void 0), this;
}
/**
* Dismantling a node means to cascade detach it.
* We always cascade detach the nodes.
* We may also cascade up the ancestry, in which case the node is detached,
* and then the parent is dismantled, leading to the whole linked structure
* ending up unlinked.
* @param includeAncestry
* Should we cascade through the ancestry (true) or only cascade through the nodes (false)?
* No default value is because the caller should always make an active choice.
*/
dismantle(includeAncestry) {
if (!this.isRoot && includeAncestry) {
const parent = __privateGet(this, _parent);
this.detachSelf(), parent.dismantle(!0);
}
for (const descendant of this.getDescendants(!1))
descendant.detachSelf();
return this;
}
//#endregion
//#region accessors
/** The item is the subject/content of the node. */
get item() {
return __privateGet(this, _item);
}
/** Get the parent node, if any. */
getParent() {
return __privateGet(this, _parent);
}
/** Get the parent item, if any. */
getParentItem() {
var _a;
return (_a = __privateGet(this, _parent)) == null ? void 0 : _a.item;
}
/** Get all child nodes. */
getChildren() {
return __privateGet(this, _children) ? [...__privateGet(this, _children)] : [];
}
/** Get all child items. */
getChildItems() {
return __privateGet(this, _children) ? nodesToItems(__privateGet(this, _children).values()) : [];
}
/** Get ancestor nodes by traversing according to the options. */
getAncestors(includeSelf = !1) {
return flattenSequence({
first: includeSelf ? this : __privateGet(this, _parent),
next: (node) => node == null ? void 0 : node.getParent()
});
}
/** Get ancestor items by traversing according to the options. */
getAncestorItems(includeSelf = !1) {
return this.getAncestors(includeSelf).map((node) => node.item);
}
/** Get descendant nodes by traversing according to the options. */
getDescendants(includeSelf = !1, type = "breadth-first") {
return flattenGraph({
roots: __privateMethod(this, _HCNode_instances, getRoots_fn).call(this, includeSelf),
next: (node) => __privateGet(node, _children),
type
});
}
/** Get descendant items by traversing according to the options. */
getDescendantItems(includeSelf = !1, type = "breadth-first") {
return this.getDescendants(includeSelf, type).map((node) => node.item);
}
/** Find the first ancestor node matching the `search`. */
findAncestor(search, includeSelf = !1) {
return searchSequence({
first: includeSelf ? this : __privateGet(this, _parent),
next: (node) => node.getParent(),
search
});
}
/** Find the ancestor nodes matching the `search`. */
findAncestors(search, includeSelf = !1) {
return searchSequenceMany({
first: includeSelf ? this : __privateGet(this, _parent),
next: (node) => node.getParent(),
search
});
}
/** Find the first descendant node matching the `search`. */
findDescendant(search, includeSelf = !1, type = "breadth-first") {
return searchGraph({
roots: __privateMethod(this, _HCNode_instances, getRoots_fn).call(this, includeSelf),
next: (node) => __privateGet(node, _children),
search,
type
});
}
/** Find the descendant nodes matching the `search`. */
findDescendants(search, includeSelf = !1, type = "breadth-first") {
return searchGraphMany({
roots: __privateMethod(this, _HCNode_instances, getRoots_fn).call(this, includeSelf),
next: (node) => __privateGet(node, _children),
search,
type
});
}
/** Does an ancestor node matching the `search` exist? */
hasAncestor(search, includeSelf = !1) {
return this.findAncestor(search, includeSelf) !== void 0;
}
/** Does a descendant node matching the `search` exist? */
hasDescendant(search, includeSelf = !1, type = "breadth-first") {
return this.findDescendant(search, includeSelf, type) !== void 0;
}
//#endregion
//#region traversal
/** Generate a sequence of ancestor nodes by traversing according to the options. */
traverseAncestors(includeSelf = !1) {
return traverseSequence({
first: includeSelf ? this : __privateGet(this, _parent),
next: (node) => node == null ? void 0 : node.getParent()
});
}
/** Generate a sequence of descendant nodes by traversing according to the options. */
traverseDescendants(includeSelf = !1, type = "breadth-first") {
return traverseFullGraph({
roots: __privateMethod(this, _HCNode_instances, getRoots_fn).call(this, includeSelf),
next: (node) => __privateGet(node, _children),
type
});
}
/**
* Get nodes to use as roots.
*/
static getRoots(roots, includeSelf = !1) {
if (includeSelf)
return roots;
if (isSomeItem(roots))
return __privateGet(roots, _children) ? [...__privateGet(roots, _children)] : [];
const children = [];
for (const root of roots)
__privateGet(root, _children) && children.push(...__privateGet(root, _children));
return children;
}
//#endregion
}
_item = new WeakMap(), _parent = new WeakMap(), _children = new WeakMap(), _brand = new WeakMap(), _HCNode_instances = new WeakSet(), //#endregion
//#region helpers
/**
* Get nodes to use as roots.
*
* Note: This must be a private method as it could otherwise be exploited to modify the `#children`.
*/
getRoots_fn = function(includeSelf = !1) {
return includeSelf ? this : __privateGet(this, _children) ?? [];
};
export {
HCNode
};
//# sourceMappingURL=node.js.map