@bunbox/tree
Version:
Tree data structure implementation for Bunbox
445 lines (439 loc) • 11.6 kB
JavaScript
// ../utils/dist/index.js
class DirtyState {
#isDirty = false;
get isDirty() {
return this.#isDirty;
}
markAsDirty() {
this.#isDirty = true;
return this;
}
unmarkAsDirty() {
this.#isDirty = false;
return this;
}
}
class EventEmitter extends DirtyState {
#isDisposed = false;
#listeners = new Map;
constructor() {
super();
}
get isDisposed() {
return this.#isDisposed;
}
subscribe(eventName, listener) {
this.on(eventName, listener);
return () => {
this.off(eventName, listener);
};
}
emit(eventName, ...args) {
if (this.#isDisposed)
return this;
const set = this.#listeners.get(eventName);
if (!set || set.size === 0)
return this;
const snapshot = [...set];
for (const listener of snapshot) {
try {
listener(...args);
} catch (err) {
console.error(`Unhandled error in event "${String(eventName)}":`, err);
}
}
return this;
}
async asyncEmit(eventName, ...args) {
if (this.#isDisposed)
return this;
const set = this.#listeners.get(eventName);
if (!set || set.size === 0)
return this;
const snapshot = [...set];
const promises = [];
for (const listener of snapshot) {
try {
const possiblePromise = listener(...args);
if (possiblePromise && possiblePromise instanceof Promise)
promises.push(possiblePromise);
} catch (err) {
console.error(`Unhandled error in event "${String(eventName)}":`, err);
}
}
await Promise.allSettled(promises);
}
on(eventName, listener) {
if (this.#isDisposed)
return this;
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, new Set);
}
this.#listeners.get(eventName).add(listener);
return this;
}
off(eventName, listener) {
if (this.#isDisposed)
return this;
const listeners = this.#listeners.get(eventName);
if (listeners) {
listeners.delete(listener);
if (listeners.size === 0) {
this.#listeners.delete(eventName);
}
}
return this;
}
once(eventName, listener) {
if (this.#isDisposed)
return this;
const onceListener = (...args) => {
listener(...args);
this.off(eventName, onceListener);
};
return this.on(eventName, onceListener);
}
clearListeners(eventName) {
if (this.#isDisposed)
return this;
if (eventName) {
this.#listeners.delete(eventName);
} else {
this.#listeners.clear();
}
return this;
}
hasListeners(eventName) {
const set = this.#listeners.get(eventName);
return Boolean(set && set.size > 0);
}
listenerCount(eventName) {
if (eventName)
return this.#listeners.get(eventName)?.size ?? 0;
let total = 0;
for (const set of this.#listeners.values())
total += set.size;
return total;
}
async dispose() {
if (this.#isDisposed)
return;
this.emit("dispose");
this.#isDisposed = true;
this.#listeners.clear();
}
}
// node_modules/ulid/dist/browser/index.js
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
var ENCODING_LEN = 32;
var RANDOM_LEN = 16;
var TIME_LEN = 10;
var TIME_MAX = 281474976710655;
var ULIDErrorCode;
(function(ULIDErrorCode2) {
ULIDErrorCode2["Base32IncorrectEncoding"] = "B32_ENC_INVALID";
ULIDErrorCode2["DecodeTimeInvalidCharacter"] = "DEC_TIME_CHAR";
ULIDErrorCode2["DecodeTimeValueMalformed"] = "DEC_TIME_MALFORMED";
ULIDErrorCode2["EncodeTimeNegative"] = "ENC_TIME_NEG";
ULIDErrorCode2["EncodeTimeSizeExceeded"] = "ENC_TIME_SIZE_EXCEED";
ULIDErrorCode2["EncodeTimeValueMalformed"] = "ENC_TIME_MALFORMED";
ULIDErrorCode2["PRNGDetectFailure"] = "PRNG_DETECT";
ULIDErrorCode2["ULIDInvalid"] = "ULID_INVALID";
ULIDErrorCode2["Unexpected"] = "UNEXPECTED";
ULIDErrorCode2["UUIDInvalid"] = "UUID_INVALID";
})(ULIDErrorCode || (ULIDErrorCode = {}));
class ULIDError extends Error {
constructor(errorCode, message) {
super(`${message} (${errorCode})`);
this.name = "ULIDError";
this.code = errorCode;
}
}
function randomChar(prng) {
const randomPosition = Math.floor(prng() * ENCODING_LEN) % ENCODING_LEN;
return ENCODING.charAt(randomPosition);
}
function detectPRNG(root) {
const rootLookup = detectRoot();
const globalCrypto = rootLookup && (rootLookup.crypto || rootLookup.msCrypto) || null;
if (typeof globalCrypto?.getRandomValues === "function") {
return () => {
const buffer = new Uint8Array(1);
globalCrypto.getRandomValues(buffer);
return buffer[0] / 255;
};
} else if (typeof globalCrypto?.randomBytes === "function") {
return () => globalCrypto.randomBytes(1).readUInt8() / 255;
} else
;
throw new ULIDError(ULIDErrorCode.PRNGDetectFailure, "Failed to find a reliable PRNG");
}
function detectRoot() {
if (inWebWorker())
return self;
if (typeof window !== "undefined") {
return window;
}
if (typeof global !== "undefined") {
return global;
}
if (typeof globalThis !== "undefined") {
return globalThis;
}
return null;
}
function encodeRandom(len, prng) {
let str = "";
for (;len > 0; len--) {
str = randomChar(prng) + str;
}
return str;
}
function encodeTime(now, len = TIME_LEN) {
if (isNaN(now)) {
throw new ULIDError(ULIDErrorCode.EncodeTimeValueMalformed, `Time must be a number: ${now}`);
} else if (now > TIME_MAX) {
throw new ULIDError(ULIDErrorCode.EncodeTimeSizeExceeded, `Cannot encode a time larger than ${TIME_MAX}: ${now}`);
} else if (now < 0) {
throw new ULIDError(ULIDErrorCode.EncodeTimeNegative, `Time must be positive: ${now}`);
} else if (Number.isInteger(now) === false) {
throw new ULIDError(ULIDErrorCode.EncodeTimeValueMalformed, `Time must be an integer: ${now}`);
}
let mod, str = "";
for (let currentLen = len;currentLen > 0; currentLen--) {
mod = now % ENCODING_LEN;
str = ENCODING.charAt(mod) + str;
now = (now - mod) / ENCODING_LEN;
}
return str;
}
function inWebWorker() {
return typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
}
function ulid(seedTime, prng) {
const currentPRNG = prng || detectPRNG();
const seed = !seedTime || isNaN(seedTime) ? Date.now() : seedTime;
return encodeTime(seed, TIME_LEN) + encodeRandom(RANDOM_LEN, currentPRNG);
}
// src/index.ts
var NODE_ID_MAP = new Map;
var NODE_NAME_MAP = new Map;
var NODE_TYPE_MAP = new Map;
function setOnMap(key, node, map) {
if (!key)
return;
if (!map.has(key)) {
map.set(key, new Set);
}
map.get(key).add(node);
}
function deleteOnMap(key, node, map) {
if (!key)
return;
if (map.has(key)) {
map.get(key).delete(node);
if (map.get(key).size === 0) {
map.delete(key);
}
}
}
class AbstractNode extends EventEmitter {
#id;
#properties;
metadata = {};
#name = "";
#parent = null;
#children = new Set;
#enabled = true;
#bindMap = new Map;
constructor(name) {
super();
this.#id = ulid();
this.#properties = new Proxy({}, {
set: (target, prop, value) => {
const key = prop;
if (target[key] !== value) {
target[key] = value;
this.markAsDirty();
}
return true;
},
deleteProperty: (target, prop) => {
const key = prop;
if (key in target) {
delete target[key];
this.markAsDirty();
}
return true;
}
});
NODE_ID_MAP.set(this.#id, this);
setOnMap(this._getType(), this, NODE_TYPE_MAP);
if (name) {
this.#name = name;
setOnMap(name, this, NODE_NAME_MAP);
}
this.on("dispose", () => {
NODE_ID_MAP.delete(this.#id);
deleteOnMap(this.#name, this, NODE_NAME_MAP);
deleteOnMap(this._getType(), this, NODE_TYPE_MAP);
if (this.#parent) {
this.#parent.removeChild(this);
}
const snapshot = [...this.#children];
for (const child of snapshot) {
child.dispose();
}
this.#children.clear();
});
}
get id() {
return this.#id;
}
get properties() {
return this.#properties;
}
get parent() {
return this.#parent;
}
get children() {
return Object.freeze([...this.#children]);
}
get name() {
return this.#name;
}
set name(value) {
if (this.#name === value)
return;
const oldName = this.#name;
this.#name = value;
this.markAsDirty();
deleteOnMap(oldName, this, NODE_NAME_MAP);
setOnMap(value, this, NODE_NAME_MAP);
this.emit("rename", this, oldName, value);
}
get isEnabled() {
return this.#enabled;
}
enable() {
return this.setEnabled(true);
}
disable() {
return this.setEnabled(false);
}
setEnabled(value) {
if (this.#enabled === value)
return this;
const prev = this.#enabled;
this.#enabled = value;
this.markAsDirty();
this.emit("enabled-change", this);
return this;
}
#isAncestorOf(node) {
let curr = node.#parent;
while (curr) {
if (curr === this)
return true;
curr = curr.#parent;
}
return false;
}
addChild(child) {
if (child === this) {
throw new Error("Cannot add a node as a child of itself");
}
if (child.#isAncestorOf(this)) {
throw new Error("Cannot add an ancestor as a child (cycle detected)");
}
if (child.#parent) {
child.#parent.removeChild(child);
}
if (child.#parent === this)
return true;
if (this.#children.has(child))
return true;
this.#children.add(child);
child.#parent = this;
this.markAsDirty();
this.emit("add-child", child);
const unsubscribe = [
child.subscribe("rename", (c, prev, next) => {
this.markAsDirty();
this.emit("rename", c, prev, next);
}),
child.subscribe("add-child", (c) => {
this.markAsDirty();
this.emit("add-child", c);
}),
child.subscribe("remove-child", (c) => {
this.markAsDirty();
this.emit("remove-child", c);
}),
child.subscribe("enabled-change", (c) => {
this.markAsDirty();
this.emit("enabled-change", c);
})
];
this.#bindMap.set(child.id, () => {
for (const off of unsubscribe)
off();
});
child._ready();
return true;
}
removeChild(child) {
if (!this.#children.has(child)) {
return false;
}
this.#children.delete(child);
child.#parent = null;
const unbind = this.#bindMap.get(child.id);
if (unbind) {
unbind();
this.#bindMap.delete(child.id);
}
this.markAsDirty();
this.emit("remove-child", child);
return true;
}
getById(id) {
return this.#id === id ? this : NODE_ID_MAP.get(id) ?? null;
}
findByName(name) {
return [...NODE_NAME_MAP.get(name) ?? []];
}
findByType(type) {
return [...NODE_TYPE_MAP.get(type.name) ?? []];
}
getRoot() {
let node = this;
while (node.#parent) {
node = node.#parent;
}
return node;
}
traverse(visitor, options) {
const includeDisabled = Boolean(options?.includeDisabled);
const order = options?.order ?? "pre";
if (!includeDisabled && !this.isEnabled)
return;
if (order === "pre")
visitor(this);
for (const child of this.#children) {
child.traverse(visitor, { includeDisabled, order });
}
if (order === "post")
visitor(this);
}
_ready() {}
}
class Node extends AbstractNode {
_getType() {
return "Node";
}
}
export {
Node,
AbstractNode
};