@ztimson/momentum
Version:
Client library for momentum
1,422 lines • 171 kB
JavaScript
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.momentum = {}));
})(this, (function(exports2) {
"use strict";
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((