UNPKG

@bunbox/tree

Version:

Tree data structure implementation for Bunbox

445 lines (439 loc) 11.6 kB
// ../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 };