UNPKG

@ztimson/momentum

Version:

Client library for momentum

1,511 lines (1,510 loc) 161 kB
var dist = {}; var persist = {}; var hasRequiredPersist; function requirePersist() { if (hasRequiredPersist) return persist; hasRequiredPersist = 1; Object.defineProperty(persist, "__esModule", { value: true }); persist.persist = persist.Persist = void 0; class Persist { key; options; /** Backend service to store data, must implement `Storage` interface */ storage; /** Listeners which should be notified on changes */ watches = {}; /** Private value field */ _value; /** Current value or default if undefined */ get value() { return this._value !== void 0 ? this._value : this.options?.default; } /** Set value with proxy object wrapper to sync future changes */ set value(v) { if (v == null || typeof v != "object") this._value = v; else this._value = new Proxy(v, { get: (target, p) => { const f = typeof target[p] == "function"; if (!f) return target[p]; return (...args) => { const value = target[p](...args); this.save(); return value; }; }, set: (target, p, newValue) => { target[p] = newValue; this.save(); return true; } }); this.save(); } /** * @param {string} key Primary key value will be stored under * @param {PersistOptions<T>} options Configure using {@link PersistOptions} */ constructor(key, options = {}) { this.key = key; this.options = options; this.storage = options.storage || localStorage; this.load(); } /** Notify listeners of change */ notify(value) { Object.values(this.watches).forEach((watch) => watch(value)); } /** Delete value from storage */ clear() { this.storage.removeItem(this.key); } /** Save current value to storage */ save() { if (this._value === void 0) this.clear(); else this.storage.setItem(this.key, JSON.stringify(this._value)); this.notify(this.value); } /** Load value from storage */ load() { if (this.storage[this.key] != void 0) { let value = JSON.parse(this.storage.getItem(this.key)); if (value != null && typeof value == "object" && this.options.type) value.__proto__ = this.options.type.prototype; this.value = value; } else this.value = this.options.default || void 0; } /** * Callback function which is run when there are changes * * @param {(value: T) => any} fn Callback will run on each change; it's passed the next value & it's return is ignored * @returns {() => void} Function which will unsubscribe the watch/callback when called */ watch(fn2) { const index = Object.keys(this.watches).length; this.watches[index] = fn2; return () => { delete this.watches[index]; }; } /** * Return value as JSON string * * @returns {string} Stringified object as JSON */ toString() { return JSON.stringify(this.value); } /** * Return current value * * @returns {T} Current value */ valueOf() { return this.value; } } persist.Persist = Persist; function persist$1(options) { return (target, prop) => { const key = options?.key || `${target.constructor.name}.${prop.toString()}`; const wrapper = new Persist(key, options); Object.defineProperty(target, prop, { get: function() { return wrapper.value; }, set: function(v) { wrapper.value = v; } }); }; } persist.persist = persist$1; return persist; } var memoryStorage = {}; var hasRequiredMemoryStorage; function requireMemoryStorage() { if (hasRequiredMemoryStorage) return memoryStorage; hasRequiredMemoryStorage = 1; Object.defineProperty(memoryStorage, "__esModule", { value: true }); memoryStorage.MemoryStorage = void 0; class MemoryStorage { get length() { return Object.keys(this).length; } clear() { Object.keys(this).forEach((k) => this.removeItem(k)); } getItem(key) { return this[key]; } key(index) { return Object.keys(this)[index]; } removeItem(key) { delete this[key]; } setItem(key, value) { this[key] = value; } } memoryStorage.MemoryStorage = MemoryStorage; return memoryStorage; } var hasRequiredDist; function requireDist() { if (hasRequiredDist) return dist; hasRequiredDist = 1; (function(exports$1) { var __createBinding = dist && dist.__createBinding || (Object.create ? (function(o, m, k, k2) { if (k2 === void 0) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === void 0) k2 = k; o[k2] = m[k]; })); var __exportStar = dist && dist.__exportStar || function(m, exports$12) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports$12, p)) __createBinding(exports$12, m, p); }; Object.defineProperty(exports$1, "__esModule", { value: true }); __exportStar(requirePersist(), exports$1); __exportStar(requireMemoryStorage(), exports$1); })(dist); return dist; } requireDist(); function JSONAttemptParse(json, fallback) { try { return JSON.parse(json); } catch { return fallback ?? json; } } function JSONSanitize(obj, space) { const cache = []; return JSON.stringify(obj, (key, value) => { if (typeof value === "object" && value !== null) { if (cache.includes(value)) return "[Circular]"; cache.push(value); } return value; }, space); } function clean(obj, undefinedOnly = false) { if (obj == null) throw new Error("Cannot clean a NULL value"); if (Array.isArray(obj)) { obj = obj.filter((o) => undefinedOnly ? o !== void 0 : o != null); } else { Object.entries(obj).forEach(([key, value]) => { if (undefinedOnly && value === void 0 || !undefinedOnly && value == null) delete obj[key]; }); } return obj; } function deepCopy(value) { if (value == null) return value; const t = typeof value; if (t === "string" || t === "number" || t === "boolean" || t === "function") return value; try { return structuredClone(value); } catch { return JSON.parse(JSONSanitize(value)); } } function dotNotation(obj, prop, set) { if (obj == null || !prop) return void 0; return prop.split(/[.[\]]/g).filter((prop2) => prop2.length).reduce((obj2, prop2, i, arr) => { if (prop2[0] == '"' || prop2[0] == "'") prop2 = prop2.slice(1, -1); if (!obj2?.hasOwnProperty(prop2)) { return void 0; } return obj2[prop2]; }, obj); } function includes(target, values, allowMissing = false) { if (target == void 0) return allowMissing; if (Array.isArray(values)) return values.findIndex((e, i) => !includes(target[i], values[i], allowMissing)) == -1; const type = typeof values; if (type != typeof target) return false; if (type == "object") { return Object.keys(values).find((key) => !includes(target[key], values[key], allowMissing)) == null; } if (type == "function") return target.toString() == values.toString(); return target == values; } function isEqual(a, b, seen = /* @__PURE__ */ new WeakMap()) { if (a === b) return true; if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags; if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) { if (Number.isNaN(a) && Number.isNaN(b)) return true; if (typeof a === "function" && typeof b === "function") return a.toString() === b.toString(); return false; } if (seen.has(a)) return seen.get(a) === b; seen.set(a, b); const isArrayA = Array.isArray(a); const isArrayB = Array.isArray(b); if (isArrayA && isArrayB) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!isEqual(a[i], b[i], seen)) return false; } return true; } if (isArrayA !== isArrayB) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key], seen)) return false; } return true; } class ASet extends Array { /** Number of elements in set */ get size() { return this.length; } /** * Array to create set from, duplicate values will be removed * @param {T[]} elements Elements which will be added to set */ constructor(elements = []) { super(); if (!!elements?.["forEach"]) elements.forEach((el) => this.add(el)); } /** * Add elements to set if unique * @param items */ add(...items) { items.filter((el) => !this.has(el)).forEach((el) => this.push(el)); return this; } /** * Remove all elements */ clear() { this.splice(0, this.length); return this; } /** * Delete elements from set * @param items Elements that will be deleted */ delete(...items) { items.forEach((el) => { const index = this.indexOf(el); if (index != -1) this.splice(index, 1); }); return this; } /** * Create list of elements this set has which the comparison set does not * @param {ASet<T>} set Set to compare against * @return {ASet<T>} Different elements */ difference(set) { return new ASet(this.filter((el) => !set.has(el))); } /** * Check if set includes element * @param {T} el Element to look for * @return {boolean} True if element was found, false otherwise */ has(el) { return this.indexOf(el) != -1; } /** * Find index number of element, or -1 if it doesn't exist. Matches by equality not reference * * @param {T} search Element to find * @param {number} fromIndex Starting index position * @return {number} Element index number or -1 if missing */ indexOf(search2, fromIndex) { return super.findIndex((el) => isEqual(el, search2), fromIndex); } /** * Create list of elements this set has in common with the comparison set * @param {ASet<T>} set Set to compare against * @return {boolean} Set of common elements */ intersection(set) { return new ASet(this.filter((el) => set.has(el))); } /** * Check if this set has no elements in common with the comparison set * @param {ASet<T>} set Set to compare against * @return {boolean} True if nothing in common, false otherwise */ isDisjointFrom(set) { return this.intersection(set).size == 0; } /** * Check if all elements in this set are included in the comparison set * @param {ASet<T>} set Set to compare against * @return {boolean} True if all elements are included, false otherwise */ isSubsetOf(set) { return this.findIndex((el) => !set.has(el)) == -1; } /** * Check if all elements from comparison set are included in this set * @param {ASet<T>} set Set to compare against * @return {boolean} True if all elements are included, false otherwise */ isSuperset(set) { return set.findIndex((el) => !this.has(el)) == -1; } /** * Create list of elements that are only in one set but not both (XOR) * @param {ASet<T>} set Set to compare against * @return {ASet<T>} New set of unique elements */ symmetricDifference(set) { return new ASet([...this.difference(set), ...set.difference(this)]); } /** * Create joined list of elements included in this & the comparison set * @param {ASet<T>} set Set join * @return {ASet<T>} New set of both previous sets combined */ union(set) { return new ASet([...this, ...set]); } } function findByProp(prop, value) { return (v) => isEqual(dotNotation(v, prop), value); } function makeArray(value) { return Array.isArray(value) ? value : [value]; } class Cache { /** * Create new cache * @param {keyof T} key Default property to use as primary key * @param options */ constructor(key, options = {}) { this.key = key; this.options = options; if (this.options.persistentStorage != null) { if (typeof this.options.persistentStorage == "string") this.options.persistentStorage = { storage: localStorage, key: this.options.persistentStorage }; if (this.options.persistentStorage?.storage?.database != void 0) { (async () => { const persists = this.options.persistentStorage; const table = await persists.storage.createTable({ name: persists.key, key: this.key }); const rows = await table.getAll(); for (const row of rows) this.store.set(this.getKey(row), row); this._loading(); })(); } else if (this.options.persistentStorage?.storage?.getItem != void 0) { const { storage, key: key2 } = this.options.persistentStorage; const stored = storage.getItem(key2); if (stored != null) { try { const obj = JSON.parse(stored); for (const k of Object.keys(obj)) this.store.set(k, obj[k]); } catch { } } this._loading(); } } else { this._loading(); } return new Proxy(this, { get: (target, prop) => { if (prop in target) return target[prop]; return this.get(prop, true); }, set: (target, prop, value) => { if (prop in target) target[prop] = value; else this.set(prop, value); return true; } }); } key; options; _loading; store = /* @__PURE__ */ new Map(); timers = /* @__PURE__ */ new Map(); lruOrder = []; /** Whether cache is complete */ complete = false; /** Await initial loading */ loading = new Promise((r) => this._loading = r); getKey(value) { if (!this.key) throw new Error("No key defined"); if (value[this.key] === void 0) throw new Error(`${this.key.toString()} Doesn't exist on ${JSON.stringify(value, null, 2)}`); return value[this.key]; } /** Save item to storage */ save(key) { const persists = this.options.persistentStorage; if (!!persists?.storage) { if (persists.storage?.database != void 0) { persists.storage.createTable({ name: persists.key, key: this.key }).then((table) => { if (key !== void 0) { const value = this.get(key, true); if (value != null) table.set(value, key); else table.delete(key); } else { table.clear(); this.all(true).forEach((row) => table.add(row)); } }); } else if (persists.storage?.setItem != void 0) { const obj = {}; for (const [k, v] of this.store.entries()) obj[k] = v; persists.storage.setItem(persists.key, JSONSanitize(obj)); } } } clearTimer(key) { const t = this.timers.get(key); if (t) { clearTimeout(t); this.timers.delete(key); } } touchLRU(key) { if (!this.options.sizeLimit || this.options.sizeLimit <= 0) return; const idx = this.lruOrder.indexOf(key); if (idx >= 0) this.lruOrder.splice(idx, 1); this.lruOrder.push(key); while (this.lruOrder.length > (this.options.sizeLimit || 0)) { const lru = this.lruOrder.shift(); if (lru !== void 0) this.delete(lru); } } /** * Get all cached items * @return {T[]} Array of items */ all(expired) { const out = []; for (const v of this.store.values()) { const val = v; if (expired || !val?._expired) out.push(deepCopy(val)); } return out; } /** * Add a new item to the cache. Like set, but finds key automatically */ add(value, ttl = this.ttl) { const key = this.getKey(value); this.set(key, value, ttl); return this; } /** * Add several rows to the cache */ addAll(rows, complete = true) { this.clear(); rows.forEach((r) => this.add(r)); this.complete = complete; return this; } /** Remove all keys */ clear() { this.complete = false; for (const [k, t] of this.timers) clearTimeout(t); this.timers.clear(); this.lruOrder = []; this.store.clear(); this.save(); return this; } /** Delete a cached item */ delete(key) { this.clearTimer(key); const idx = this.lruOrder.indexOf(key); if (idx >= 0) this.lruOrder.splice(idx, 1); this.store.delete(key); this.save(key); return this; } /** Return entries as array */ entries(expired) { const out = []; for (const [k, v] of this.store.entries()) { const val = v; if (expired || !val?._expired) out.push([k, deepCopy(val)]); } return out; } /** Manually expire a cached item */ expire(key) { this.complete = false; if (this.options.expiryPolicy == "keep") { const v = this.store.get(key); if (v) { v._expired = true; this.store.set(key, v); this.save(key); } } else this.delete(key); return this; } /** Find first matching item */ find(filter, expired) { for (const v of this.store.values()) { const row = v; if ((expired || !row._expired) && includes(row, filter)) return deepCopy(row); } return void 0; } /** Get cached item by key */ get(key, expired) { const raw = this.store.get(key); if (raw == null) return null; this.touchLRU(key); const isExpired = raw?._expired; if (expired || !isExpired) return deepCopy(raw); return null; } /** Return list of keys */ keys(expired) { const out = []; for (const [k, v] of this.store.entries()) { const val = v; if (expired || !val?._expired) out.push(k); } return out; } /** Return map of key → item */ map(expired) { const copy = {}; for (const [k, v] of this.store.entries()) { const val = v; if (expired || !val?._expired) copy[k] = deepCopy(val); } return copy; } /** Add item manually specifying the key */ set(key, value, ttl = this.options.ttl) { if (this.options.expiryPolicy == "keep") delete value._expired; this.clearTimer(key); this.store.set(key, value); this.touchLRU(key); this.save(key); if (ttl) { const t = setTimeout(() => { this.expire(key); this.save(key); }, ttl * 1e3); this.timers.set(key, t); } return this; } /** Get all cached items */ values = this.all; } function contrast(color) { const exploded = color?.match(color.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g); if (!exploded || exploded?.length < 3) return "black"; const [r, g, b] = exploded.map((hex) => parseInt(hex.length == 1 ? `${hex}${hex}` : hex, 16)); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? "black" : "white"; } const LETTER_LIST = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const NUMBER_LIST = "0123456789"; const SYMBOL_LIST = "~`!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/"; function formatBytes(bytes, decimals = 2) { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; } function randomStringBuilder(length, letters = false, numbers = false, symbols = false) { if (!letters && !numbers && !symbols) throw new Error("Must enable at least one: letters, numbers, symbols"); return Array(length).fill(null).map(() => { let c; do { const type = ~~(Math.random() * 3); if (letters && type == 0) { c = LETTER_LIST[~~(Math.random() * LETTER_LIST.length)]; } else if (numbers && type == 1) { c = NUMBER_LIST[~~(Math.random() * NUMBER_LIST.length)]; } else if (symbols && type == 2) { c = SYMBOL_LIST[~~(Math.random() * SYMBOL_LIST.length)]; } } while (!c); return c; }).join(""); } function dayOfWeek(d) { return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][d]; } function dayOfYear(date) { const start = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); return Math.ceil((date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)); } function formatDate(format = "YYYY-MM-DD H:mm", date = /* @__PURE__ */ new Date(), tz = "local") { if (typeof date === "number" || typeof date === "string") date = new Date(date); if (isNaN(date.getTime())) throw new Error("Invalid date input"); const numericTz = typeof tz === "number"; const localTz = tz === "local" || !numericTz && tz.toLowerCase?.() === "local"; const tzName = localTz ? Intl.DateTimeFormat().resolvedOptions().timeZone : numericTz ? "UTC" : tz; if (!numericTz && tzName !== "UTC") { try { new Intl.DateTimeFormat("en-US", { timeZone: tzName }).format(); } catch { throw new Error(`Invalid timezone: ${tzName}`); } } let zonedDate = new Date(date); let get; const partsMap = {}; if (!numericTz && tzName !== "UTC") { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tzName, year: "numeric", month: "2-digit", day: "2-digit", weekday: "long", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).formatToParts(date); parts.forEach((p) => { partsMap[p.type] = p.value; }); const monthValue = parseInt(partsMap.month) - 1; const dayOfWeekValue = (/* @__PURE__ */ new Date(`${partsMap.year}-${partsMap.month}-${partsMap.day}`)).getDay(); const hourValue = parseInt(partsMap.hour); get = (fn2) => { switch (fn2) { case "FullYear": return parseInt(partsMap.year); case "Month": return monthValue; case "Date": return parseInt(partsMap.day); case "Day": return dayOfWeekValue; case "Hours": return hourValue; case "Minutes": return parseInt(partsMap.minute); case "Seconds": return parseInt(partsMap.second); default: return 0; } }; } else { const offset = numericTz ? tz : 0; zonedDate = new Date(date.getTime() + offset * 60 * 60 * 1e3); get = (fn2) => zonedDate[`getUTC${fn2}`](); } function numSuffix2(n) { const s = ["th", "st", "nd", "rd"]; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function getTZOffset() { if (numericTz) { const total = tz * 60; const hours = Math.floor(Math.abs(total) / 60); const mins = Math.abs(total) % 60; return `${tz >= 0 ? "+" : "-"}${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`; } try { const offset = new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "longOffset", hour: "2-digit", minute: "2-digit" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value.match(/([+-]\d{2}:\d{2})/)?.[1]; if (offset) return offset; } catch { } return "+00:00"; } function getTZAbbr() { if (numericTz && tz === 0) return "UTC"; try { return new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "short" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value || ""; } catch { return tzName; } } const tokens = { YYYY: get("FullYear").toString(), YY: get("FullYear").toString().slice(2), MMMM: month(get("Month")), MMM: month(get("Month")).slice(0, 3), MM: (get("Month") + 1).toString().padStart(2, "0"), M: (get("Month") + 1).toString(), DDD: dayOfYear(zonedDate).toString(), DD: get("Date").toString().padStart(2, "0"), Do: numSuffix2(get("Date")), D: get("Date").toString(), dddd: dayOfWeek(get("Day")), ddd: dayOfWeek(get("Day")).slice(0, 3), HH: get("Hours").toString().padStart(2, "0"), H: get("Hours").toString(), hh: (get("Hours") % 12 || 12).toString().padStart(2, "0"), h: (get("Hours") % 12 || 12).toString(), mm: get("Minutes").toString().padStart(2, "0"), m: get("Minutes").toString(), ss: get("Seconds").toString().padStart(2, "0"), s: get("Seconds").toString(), SSS: zonedDate[`getUTC${"Milliseconds"}`]().toString().padStart(3, "0"), A: get("Hours") >= 12 ? "PM" : "AM", a: get("Hours") >= 12 ? "pm" : "am", ZZ: getTZOffset().replace(":", ""), Z: getTZOffset(), z: getTZAbbr() }; return format.replace(/YYYY|YY|MMMM|MMM|MM|M|DDD|DD|Do|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|SSS|A|a|ZZ|Z|z/g, (token) => tokens[token]); } function month(m) { return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][m]; } function sleep(ms) { return new Promise((res) => setTimeout(res, ms)); } async function sleepWhile(fn2, checkInterval = 100) { while (await fn2()) await sleep(checkInterval); } class AsyncLock { p = Promise.resolve(); run(fn2) { const res = this.p.then(fn2, fn2); this.p = res.then(() => { }, () => { }); return res; } } class Database { constructor(database, tables, version2) { this.database = database; this.version = version2; this.connection = new Promise((resolve, reject) => { let req; try { req = indexedDB.open(this.database, this.version); } catch (err) { return reject(err); } this.tables = !tables ? [] : tables.map((t) => { t = typeof t == "object" ? t : { name: t }; return { ...t, name: t.name.toString() }; }); req.onerror = () => reject(req.error); req.onsuccess = () => { let db; try { db = req.result; } catch (err) { return reject(err); } const existing = Array.from(db.objectStoreNames); if (!tables) { this.tables = existing.map((t) => { try { const tx = db.transaction(t, "readonly"); const store = tx.objectStore(t); return { name: t, key: store.keyPath }; } catch { return { name: t }; } }); } const desired = new ASet((tables || []).map((t) => typeof t == "string" ? t : t.name)); if (tables && desired.symmetricDifference(new ASet(existing)).length) { db.close(); Object.assign(this, new Database(this.database, this.tables, db.version + 1)); this.connection.then(resolve); } else { this.version = db.version; resolve(db); } this.upgrading = false; }; req.onupgradeneeded = () => { this.upgrading = true; let db; try { db = req.result; } catch { return; } try { const existingTables = new ASet(Array.from(db.objectStoreNames)); if (tables) { const desired = new ASet((tables || []).map((t) => typeof t == "string" ? t : t.name)); existingTables.difference(desired).forEach((name) => db.deleteObjectStore(name)); desired.difference(existingTables).forEach((name) => { const t = this.tables.find(findByProp("name", name)); db.createObjectStore(name, { keyPath: t?.key, autoIncrement: t?.autoIncrement || !t?.key }); }); } } catch { } }; }); } database; version; schemaLock = new AsyncLock(); upgrading = false; connection; tables; get ready() { return !this.upgrading; } waitForUpgrade = () => sleepWhile(() => this.upgrading); async createTable(table) { return this.schemaLock.run(async () => { if (typeof table == "string") table = { name: table }; const conn = await this.connection; if (!this.includes(table.name)) { const newDb = new Database(this.database, [...this.tables, table], (this.version ?? 0) + 1); conn.close(); this.connection = newDb.connection; await this.connection; Object.assign(this, newDb); } return this.table(table.name); }); } async deleteTable(table) { return this.schemaLock.run(async () => { if (typeof table == "string") table = { name: table }; if (!this.includes(table.name)) return; const conn = await this.connection; const newDb = new Database(this.database, this.tables.filter((t) => t.name != table.name), (this.version ?? 0) + 1); conn.close(); this.connection = newDb.connection; await this.connection; Object.assign(this, newDb); }); } includes(name) { return !!this.tables.find((t) => t.name == (typeof name == "object" ? name.name : name.toString())); } table(name) { return new Table(this, name.toString()); } } class Table { constructor(database, name, key = "id") { this.database = database; this.name = name; this.key = key; this.database.connection.then(() => { const exists = !!this.database.tables.find(findByProp("name", this.name)); if (!exists) this.database.createTable(this.name); }); } database; name; key; async tx(table, fn2, readonly = false) { await this.database.waitForUpgrade(); const db = await this.database.connection; const tx = db.transaction(table, readonly ? "readonly" : "readwrite"); return new Promise((resolve, reject) => { const request = fn2(tx.objectStore(table)); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } add(value, key) { return this.tx(this.name, (store) => store.add(value, key)); } all = this.getAll; clear() { return this.tx(this.name, (store) => store.clear()); } count() { return this.tx(this.name, (store) => store.count(), true); } create = this.add; delete(key) { return this.tx(this.name, (store) => store.delete(key)); } get(key) { return this.tx(this.name, (store) => store.get(key), true); } getAll() { return this.tx(this.name, (store) => store.getAll(), true); } getAllKeys() { return this.tx(this.name, (store) => store.getAllKeys(), true); } put(value, key) { return this.tx(this.name, (store) => { if (store.keyPath) return store.put(value); return store.put(value, key); }); } read(key) { return key ? this.get(key) : this.getAll(); } set(value, key) { if (key) value[this.key] = key; if (!value[this.key]) return this.add(value); return this.put(value); } update = this.set; } class PromiseProgress extends Promise { listeners = []; _progress = 0; get progress() { return this._progress; } set progress(p) { if (p == this._progress) return; this._progress = p; this.listeners.forEach((l) => l(p)); } constructor(executor) { super((resolve, reject) => executor( (value) => resolve(value), (reason) => reject(reason), (progress) => this.progress = progress )); } static from(promise) { if (promise instanceof PromiseProgress) return promise; return new PromiseProgress((res, rej) => promise.then((...args) => res(...args)).catch((...args) => rej(...args))); } from(promise) { const newPromise = PromiseProgress.from(promise); this.onProgress((p) => newPromise.progress = p); return newPromise; } onProgress(callback) { this.listeners.push(callback); return this; } then(res, rej) { const resp = super.then(res, rej); return this.from(resp); } catch(rej) { return this.from(super.catch(rej)); } finally(res) { return this.from(super.finally(res)); } } function downloadFile(blob, name) { if (!(blob instanceof Blob)) blob = new Blob(makeArray(blob)); const url = URL.createObjectURL(blob); downloadUrl(url, name); URL.revokeObjectURL(url); } function downloadUrl(href, name) { const a = document.createElement("a"); a.href = href; a.download = name || href.split("/").pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); } function fileBrowser(options = {}) { return new Promise((res) => { const input = document.createElement("input"); input.type = "file"; input.accept = options.accept || "*"; input.style.display = "none"; input.multiple = !!options.multiple; input.onblur = input.onchange = async () => { res(Array.from(input.files)); input.remove(); }; document.body.appendChild(input); input.click(); }); } function timestampFilename(name, date = /* @__PURE__ */ new Date()) { if (typeof date == "number" || typeof date == "string") date = new Date(date); const timestamp = formatDate("YYYY-MM-DD_HH-mm", date); return timestamp; } function uploadWithProgress(options) { return new PromiseProgress((res, rej, prog) => { const xhr = new XMLHttpRequest(); const formData2 = new FormData(); options.files.forEach((f) => formData2.append("file", f)); xhr.withCredentials = !!options.withCredentials; xhr.upload.addEventListener("progress", (event) => event.lengthComputable ? prog(event.loaded / event.total) : null); xhr.addEventListener("loadend", () => res(JSONAttemptParse(xhr.responseText))); xhr.addEventListener("error", () => rej(JSONAttemptParse(xhr.responseText))); xhr.addEventListener("timeout", () => rej({ error: "Request timed out" })); xhr.open("POST", options.url); Object.entries(options.headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value)); xhr.send(formData2); }); } class HttpResponse extends Response { data; ok; redirected; type; url; constructor(resp, stream) { const body = [204, 205, 304].includes(resp.status) ? null : stream; super(body, { headers: resp.headers, status: resp.status, statusText: resp.statusText }); this.ok = resp.ok; this.redirected = resp.redirected; this.type = resp.type; this.url = resp.url; } } class Http { static interceptors = {}; static headers = {}; interceptors = {}; headers = {}; url; constructor(defaults = {}) { this.url = defaults.url ?? null; this.headers = defaults.headers || {}; if (defaults.interceptors) { defaults.interceptors.forEach((i) => Http.addInterceptor(i)); } } static addInterceptor(fn2) { const key = Object.keys(Http.interceptors).length.toString(); Http.interceptors[key] = fn2; return () => { Http.interceptors[key] = null; }; } addInterceptor(fn2) { const key = Object.keys(this.interceptors).length.toString(); this.interceptors[key] = fn2; return () => { this.interceptors[key] = null; }; } request(opts = {}) { if (!this.url && !opts.url) throw new Error("URL needs to be set"); let url = opts.url?.startsWith("http") ? opts.url : (this.url || "") + (opts.url || ""); url = url.replaceAll(/([^:]\/)\/+/g, "$1"); if (opts.fragment) url.includes("#") ? url.replace(/#.*([?\n])/g, (match, arg1) => `#${opts.fragment}${arg1}`) : `${url}#${opts.fragment}`; if (opts.query) { const q = Array.isArray(opts.query) ? opts.query : Object.keys(opts.query).map((k) => ({ key: k, value: opts.query[k] })); url += (url.includes("?") ? "&" : "?") + q.map((q2) => `${q2.key}=${q2.value}`).join("&"); } const headers = clean({ "Content-Type": !opts.body ? void 0 : opts.body instanceof FormData ? "multipart/form-data" : "application/json", ...Http.headers, ...this.headers, ...opts.headers }); if (typeof opts.body == "object" && opts.body != null && headers["Content-Type"] == "application/json") opts.body = JSON.stringify(opts.body); return new PromiseProgress((res, rej, prog) => { try { fetch(url, { headers, method: opts.method || (opts.body ? "POST" : "GET"), body: opts.body }).then(async (resp) => { for (let fn2 of [...Object.values(Http.interceptors), ...Object.values(this.interceptors)]) { await new Promise((res2) => fn2(resp, () => res2())); } const contentLength = resp.headers.get("Content-Length"); const total = contentLength ? parseInt(contentLength, 10) : 0; let loaded = 0; const reader = resp.body?.getReader(); const stream = new ReadableStream({ start(controller) { function push() { reader?.read().then((event) => { if (event.done) return controller.close(); loaded += event.value.byteLength; prog(loaded / total); controller.enqueue(event.value); push(); }).catch((error) => controller.error(error)); } push(); } }); resp = new HttpResponse(resp, stream); if (opts.decode !== false) { const content = resp.headers.get("Content-Type")?.toLowerCase(); if (content?.includes("form")) resp.data = await resp.formData(); else if (content?.includes("json")) resp.data = await resp.json(); else if (content?.includes("text")) resp.data = await resp.text(); else if (content?.includes("application")) resp.data = await resp.blob(); } if (resp.ok) res(resp); else rej(resp); }).catch((err) => rej(err)); } catch (err) { rej(err); } }); } } var LOG_LEVEL = /* @__PURE__ */ ((LOG_LEVEL2) => { LOG_LEVEL2[LOG_LEVEL2["ERROR"] = 0] = "ERROR"; LOG_LEVEL2[LOG_LEVEL2["WARN"] = 1] = "WARN"; LOG_LEVEL2[LOG_LEVEL2["INFO"] = 2] = "INFO"; LOG_LEVEL2[LOG_LEVEL2["LOG"] = 3] = "LOG"; LOG_LEVEL2[LOG_LEVEL2["DEBUG"] = 4] = "DEBUG"; return LOG_LEVEL2; })(LOG_LEVEL || {}); function PE(str, ...args) { const combined = []; for (let i = 0; i < str.length || i < args.length; i++) { if (str[i]) combined.push(str[i]); if (args[i]) combined.push(args[i]); } return new PathEvent(combined.join("/")); } function PES(str, ...args) { let combined = []; for (let i = 0; i < str.length || i < args.length; i++) { if (str[i]) combined.push(str[i]); if (args[i]) combined.push(args[i]); } const [paths, methods] = combined.join("/").split(":"); return PathEvent.toString(paths, methods?.split("")); } class PathError extends Error { } class PathEvent { /** First directory in path */ module; /** Entire path, including the module & name */ fullPath; /** Parent directory, excludes module & name */ dir; /** Path including the name, excluding the module */ path; /** Last segment of path */ name; /** List of methods */ methods; /** Whether this path contains glob patterns */ hasGlob; /** Internal cache for PathEvent instances to avoid redundant parsing */ static pathEventCache = /* @__PURE__ */ new Map(); /** Cache for compiled permissions (path + required permissions → result) */ static permissionCache = /* @__PURE__ */ new Map(); /** Max size for permission cache before LRU eviction */ static MAX_PERMISSION_CACHE_SIZE = 1e3; /** All/Wildcard specified */ get all() { return this.methods.has("*"); } set all(v) { v ? this.methods = new ASet(["*"]) : this.methods.delete("*"); } /** None specified */ get none() { return this.methods.has("n"); } set none(v) { v ? this.methods = new ASet(["n"]) : this.methods.delete("n"); } /** Create method specified */ get create() { return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("c")); } set create(v) { v ? this.methods.delete("n").delete("*").add("c") : this.methods.delete("c"); } /** Execute method specified */ get execute() { return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("x")); } set execute(v) { v ? this.methods.delete("n").delete("*").add("x") : this.methods.delete("x"); } /** Read method specified */ get read() { return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("r")); } set read(v) { v ? this.methods.delete("n").delete("*").add("r") : this.methods.delete("r"); } /** Update method specified */ get update() { return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("u")); } set update(v) { v ? this.methods.delete("n").delete("*").add("u") : this.methods.delete("u"); } /** Delete method specified */ get delete() { return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("d")); } set delete(v) { v ? this.methods.delete("n").delete("*").add("d") : this.methods.delete("d"); } constructor(e) { if (typeof e == "object") { Object.assign(this, e); return; } if (PathEvent.pathEventCache.has(e)) { Object.assign(this, PathEvent.pathEventCache.get(e)); return; } let [p, method] = e.replaceAll(/(^|\/)\*+\/?$/g, "").split(":"); if (!method) method = "*"; if (p === "" || p === void 0 || p === "*") { this.module = ""; this.path = ""; this.dir = ""; this.fullPath = "**"; this.name = ""; this.methods = new ASet(p === "*" ? ["*"] : method.split("")); this.hasGlob = true; PathEvent.pathEventCache.set(e, this); return; } let temp = p.split("/").filter((p2) => !!p2); this.module = temp.splice(0, 1)[0] || ""; this.path = temp.join("/"); this.dir = temp.length > 2 ? temp.slice(0, -1).join("/") : ""; this.fullPath = `${this.module}${this.module && this.path ? "/" : ""}${this.path}`; this.name = temp.pop() || ""; this.hasGlob = this.fullPath.includes("*"); this.methods = new ASet(method.split("")); PathEvent.pathEventCache.set(e, this); } /** * Check if a filter pattern matches a target path * @private */ static matches(pattern, target) { const methodsMatch = pattern.all || target.all || pattern.methods.intersection(target.methods).length > 0; if (!methodsMatch) return false; if (!pattern.hasGlob && !target.hasGlob) { const last = pattern.fullPath[target.fullPath.length]; return pattern.fullPath.startsWith(target.fullPath) && (last == null || last == "/"); } if (pattern.hasGlob) return this.pathMatchesGlob(target.fullPath, pattern.fullPath); return this.pathMatchesGlob(pattern.fullPath, target.fullPath); } /** * Check if a path matches a glob pattern * @private */ static pathMatchesGlob(path, pattern) { if (pattern === path) return true; const pathParts = path.split("/").filter((p) => !!p); const patternParts = pattern.split("/").filter((p) => !!p); let pathIdx = 0; let patternIdx = 0; while (patternIdx < patternParts.length && pathIdx < pathParts.length) { const patternPart = patternParts[patternIdx]; if (patternPart === "**") { if (patternIdx === patternParts.length - 1) return true; while (pathIdx < pathParts.length) { if (PathEvent.pathMatchesGlob(pathParts.slice(pathIdx).join("/"), patternParts.slice(patternIdx + 1).join("/"))) return true; pathIdx++; } return false; } else if (patternPart === "*") { pathIdx++; patternIdx++; } else { if (patternPart !== pathParts[pathIdx]) return false; pathIdx++; patternIdx++; } } if (patternIdx < patternParts.length) return patternParts.slice(patternIdx).every((p) => p === "**"); return pathIdx === pathParts.length; } /** * Score a path for specificity ranking (lower = more specific = higher priority) * @private */ static scoreSpecificity(path) { if (path === "**" || path === "") return Number.MAX_SAFE_INTEGER; const segments = path.split("/").filter((p) => !!p); let score = -segments.length; segments.forEach((seg) => { if (seg === "**") score += 0.5; else if (seg === "*") score += 0.25; }); return score; } /** Clear the cache of all PathEvents */ static clearCache() { PathEvent.pathEventCache.clear(); } /** Clear the permission cache */ static clearPermissionCache() { PathEvent.permissionCache.clear(); } /** * Combine multiple events into one parsed object. Longest path takes precedent, but all subsequent methods are * combined until a "none" is reached * * @param {string | PathEvent} paths Events as strings or pre-parsed * @return {PathEvent} Final combined permission */ static combine(...paths) { const parsed = paths.map((p) => p instanceof PathEvent ? p : new PathEvent(p)); const sorted = parsed.toSorted((p1, p2) => { const score1 = PathEvent.scoreSpecificity(p1.fullPath); const score2 = PathEvent.scoreSpecificity(p2.fullPath); return score1 - score2; }); let result = null; for (const p of sorted) { if (!result) { result = p; } else { if (result.fullPath.startsWith(p.fullPath)) { if (p.none) break; result.methods = new ASet([...result.methods, ...p.methods]); } } } return result || new PathEvent(""); } /** * Filter a set of paths based on the target * * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered * @param filter {...PathEvent} Must contain one of * @return {PathEvent[]} Filtered results */ static filter(target, ...filter) { const parsedTarget = makeArray(target).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe)); const parsedFilter = makeArray(filter).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe)); return parsedTarget.filter((t) => !!parsedFilter.find((r) => PathEvent.matches(r, t))); } /** * Squash 2 sets of paths & return true if any overlap is found * * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed * @param has Target must have at least one of these path * @return {boolean} Whether there is any overlap */ static has(target, ...has) { const parsedTarget = makeArray(target).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe)); const parsedRequired = makeArray(has).map((pe) => pe instanceof PathEvent ? pe : new PathEvent(pe)); return !!parsedRequired.find((r) => !!parsedTarget.find((t) => PathEvent.matches(r, t))); } /** * Squash 2 sets of paths & return true if the target has all paths * * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed * @param has Target must have all these paths * @return {boolean} Whether all are present */ static hasAll(target, ...has) { return has.filter((h) => PathEvent.has(target, h)).length == has.length; } /** * Same as `has` but raises an error if there is no overlap * * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed * @param has Target must have at least one of these path */ static hasFatal(target, ...has) { if (!PathEvent.has(target, ...has)) throw new PathError(`Requires one of: ${makeArray(has).join(", ")}`); } /** * Same as `hasAll` but raises an error if the target is missing any paths * * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed * @param has Target must have all these paths */ static hasAllFatal(target, ...has) { if (!PathEvent.hasAll(target, ...has)) throw new PathError(`Requires all: ${makeArray(has).join(", ")}`); } /** * Create event string from its components * * @param {string | string[]} path Event path * @param {Method} methods Event method * @return {string} String representation of Event */ static toString(path, methods) { let p = makeArray(path).filter((p2) => !!p2).join("/"); p = p?.trim().replaceAll(/\/{2,}/g, "/").replaceAll(/(^\/|\/$)/g, ""); if (methods?.length) p += `:${makeArray(methods).map((m) => m.toLowerCase()).join("")}`; return p; } /** * Squash 2 sets of paths & return true if any overlap is found * * @param has Target must have at least one of these path * @return {boolean} Whether there is any overlap */ has(...has) { return PathEvent.has(this, ...has); } /** * Squash 2 sets of paths & return true if the target has all paths * * @param has Target must have all these paths * @return {boolean} Whether all are present */ hasAll(...has) { return PathEvent.hasAll(this, ...has); } /** * Same as `has` but raises an error if there is no overlap * * @param has Target must have at least one of these path */ hasFatal(...has) { return PathEvent.hasFatal(this, ...has); } /** * Same as `hasAll` but raises an error if the target is missing any paths * * @param has Target must have all these paths */ hasAllFatal(...has) { return PathEvent.hasAllFatal(this, ...has); } /** * Filter a set of paths based on this event * * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered * @return {PathEvent[]} Filtered results */ filter(target) { return PathEvent.filter(target, this); } /** * Create event string from its components * * @return {string} String representation of Event */ toString() {