Compare commits

..

9 Commits

Author SHA1 Message Date
71552aa243 Fixed cache save to db
Some checks failed
Build / Build NPM Project (push) Successful in 43s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 8s
2025-07-07 15:27:07 -04:00
ce3878e18b Added db safe guards
Some checks failed
Build / Build NPM Project (push) Successful in 44s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 9s
2025-07-07 14:53:14 -04:00
388a09718b Reverted ASet
Some checks failed
Build / Build NPM Project (push) Successful in 43s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 8s
2025-07-07 14:34:29 -04:00
59bce9d28d Optimizations
Some checks failed
Build / Build NPM Project (push) Failing after 29s
Build / Publish Documentation (push) Has been skipped
Build / Tag Version (push) Has been skipped
2025-07-07 14:29:49 -04:00
387e6b2512 Fixed cache save to db
Some checks failed
Build / Build NPM Project (push) Successful in 43s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 8s
2025-07-07 12:41:44 -04:00
d748d740ea bump 0.26.0
Some checks failed
Build / Build NPM Project (push) Successful in 42s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 8s
2025-07-07 10:52:36 -04:00
c8f44584a2 Fixed cache saving with different backends
Some checks failed
Build / Build NPM Project (push) Successful in 43s
Build / Publish Documentation (push) Failing after 5s
Build / Tag Version (push) Successful in 8s
2025-07-07 10:46:54 -04:00
8b84d170a6 Fixed test
Some checks failed
Build / Build NPM Project (push) Successful in 1m11s
Build / Tag Version (push) Successful in 17s
Build / Publish Documentation (push) Failing after 19s
2025-07-07 10:42:40 -04:00
37df7e6090 Fixed an unreliable caching backend check
Some checks failed
Build / Build NPM Project (push) Failing after 45s
Build / Publish Documentation (push) Has been skipped
Build / Tag Version (push) Has been skipped
2025-07-07 10:36:47 -04:00
8 changed files with 103 additions and 38 deletions

View File

@ -1,9 +1,11 @@
<html>
<body>
<script type="module">
import {PES} from './dist/index.mjs';
import {PathEventEmitter} from './dist/index.mjs';
console.log(PES`storage${'Test/Test'}:d`);
const emitter = new PathEventEmitter('data');
emitter.on('*', console.log);
emitter.emit('data/asd', {});
</script>
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"name": "@ztimson/utils",
"version": "0.25.26",
"version": "0.26.4",
"description": "Utility library",
"author": "Zak Timson",
"license": "MIT",

View File

@ -39,7 +39,7 @@ export class Cache<K extends string | number | symbol, T> {
if(typeof this.options.persistentStorage == 'string')
this.options.persistentStorage = {storage: localStorage, key: this.options.persistentStorage};
if(this.options.persistentStorage?.storage instanceof Database) {
if(this.options.persistentStorage?.storage?.constructor.name == 'Database') {
(async () => {
const persists: any = this.options.persistentStorage;
const table: Table<any, any> = await persists.storage.createTable({name: persists.key, key: this.key});
@ -47,8 +47,8 @@ export class Cache<K extends string | number | symbol, T> {
Object.assign(this.store, rows.reduce((acc, row) => ({...acc, [this.getKey(row)]: row}), {}));
done();
})();
} else {
const stored = this.options.persistentStorage.storage.getItem(this.options.persistentStorage.key);
} else if(this.options.persistentStorage?.storage?.constructor.name == 'Storage') {
const stored = (<Storage>this.options.persistentStorage.storage).getItem(this.options.persistentStorage.key);
if(stored != null) try { Object.assign(this.store, JSON.parse(stored)); } catch { }
done();
}
@ -75,18 +75,18 @@ export class Cache<K extends string | number | symbol, T> {
}
private save(key?: K) {
const persists: any = this.options.persistentStorage;
const persists: {storage: any, key: string} = <any>this.options.persistentStorage;
if(!!persists?.storage) {
if(persists.storage instanceof Database) {
(<Database>persists.storage).createTable({name: persists.storage.key, key: <string>this.key}).then(table => {
if(persists.storage?.constructor.name == 'Database') {
(<Database>persists.storage).createTable({name: persists.key, key: <string>this.key}).then(table => {
if(key) {
table.set(key, this.get(key));
table.set(this.get(key), key);
} else {
table.clear();
this.all().forEach(row => table.add(row));
}
});
} else {
} else if(persists.storage?.constructor.name == 'Storage') {
persists.storage.setItem(persists.storage.key, JSONSanitize(this.all(true)));
}
}

View File

@ -166,8 +166,11 @@ export class Table<K extends IDBValidKey = any, T = any> {
return this.tx(this.name, store => store.getAllKeys(), true);
}
put(key: K, value: T): Promise<void> {
return this.tx(this.name, store => store.put(value, key));
put(value: T, key?: string): Promise<void> {
return this.tx(this.name, store => {
if (store.keyPath) return store.put(value);
return store.put(value, key);
});
}
read(): Promise<T[]>;
@ -177,8 +180,9 @@ export class Table<K extends IDBValidKey = any, T = any> {
}
set(value: T, key?: K): Promise<void> {
if(!key && !(<any>value)[this.key]) return this.add(value);
return this.put(key || (<any>value)[this.key], value);
if(key) (<any>value)[this.key] = key;
if(!(<any>value)[this.key]) return this.add(value);
return this.put(value);
}
update = this.set;

View File

@ -177,19 +177,51 @@ export function includes(target: any, values: any, allowMissing = false): boolea
}
/**
* Deep check if two objects are equal
* Deep check if two items are equal.
* Handles primitives, objects, arrays, functions, Date, RegExp, and circular references.
*
* @param {any} a - first item to compare
* @param {any} b - second item to compare
* @param {WeakMap<object, object>} [seen] - Internal parameter to track circular references
* @returns {boolean} True if they match
*/
export function isEqual(a: any, b: any): boolean {
const ta = typeof a, tb = typeof b;
if((ta != 'object' || a == null) || (tb != 'object' || b == null))
return ta == 'function' && tb == 'function' ? a.toString() == b.toString() : a === b;
const keys = Object.keys(a);
if(keys.length != Object.keys(b).length) return false;
return Object.keys(a).every(key => isEqual(a[key], b[key]));
export function isEqual(a: any, b: any, seen = new WeakMap<object, object>()): boolean {
// Simple cases
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;
// Null checks
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;
}
// Circular references
if(seen.has(a)) return seen.get(a) === b;
seen.set(a, b);
const isArrayA = Array.isArray(a);
const isArrayB = Array.isArray(b);
// Array checks
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;
// Key & value deep comparison
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;
}
/**

View File

@ -71,9 +71,12 @@ export class PathEvent {
/** List of methods */
methods!: ASet<Method>;
/** Internal cache for PathEvent instances to avoid redundant parsing */
private static pathEventCache: Map<string, PathEvent> = new Map();
/** All/Wildcard specified */
get all(): boolean { return this.methods.has('*') }
set all(v: boolean) { v ? new ASet<Method>(['*']) : this.methods.delete('*'); }
set all(v: boolean) { v ? this.methods = new ASet<Method>(['*']) : this.methods.delete('*'); }
/** None specified */
get none(): boolean { return this.methods.has('n') }
set none(v: boolean) { v ? this.methods = new ASet<Method>(['n']) : this.methods.delete('n'); }
@ -91,10 +94,20 @@ export class PathEvent {
set delete(v: boolean) { v ? this.methods.delete('n').delete('*').add('d') : this.methods.delete('d'); }
constructor(e: string | PathEvent) {
if(typeof e == 'object') return Object.assign(this, e);
if(typeof e == 'object') {
Object.assign(this, e);
return;
}
// Check cache first
if (PathEvent.pathEventCache.has(e)) {
Object.assign(this, PathEvent.pathEventCache.get(e)!);
return;
}
let [p, scope, method] = e.replaceAll(/\/{2,}/g, '/').split(':');
if(!method) method = scope || '*';
if(p == '*' || !p && method == '*') {
if(p == '*' || (!p && method == '*')) {
p = '';
method = '*';
}
@ -104,6 +117,14 @@ export class PathEvent {
this.fullPath = `${this.module}${this.module && this.path ? '/' : ''}${this.path}`;
this.name = temp.pop() || '';
this.methods = new ASet(<any>method.split(''));
// Store in cache
PathEvent.pathEventCache.set(e, this);
}
/** Clear the cache of all PathEvents */
static clearCache(): void {
PathEvent.pathEventCache.clear();
}
/**
@ -115,7 +136,7 @@ export class PathEvent {
*/
static combine(...paths: (string | PathEvent)[]): PathEvent {
let hitNone = false;
const combined = paths.map(p => new PathEvent(p))
const combined = paths.map(p => p instanceof PathEvent ? p : new PathEvent(p))
.toSorted((p1, p2) => {
const l1 = p1.fullPath.length, l2 = p2.fullPath.length;
return l1 < l2 ? 1 : (l1 > l2 ? -1 : 0);
@ -124,10 +145,9 @@ export class PathEvent {
if(p.none) hitNone = true;
if(!acc) return p;
if(hitNone) return acc;
acc.methods = [...acc.methods, ...p.methods];
acc.methods = new ASet([...acc.methods, ...p.methods]);
return acc;
}, <any>null);
combined.methods = new ASet<Method>(combined.methods);
return combined;
}
@ -139,11 +159,12 @@ export class PathEvent {
* @return {boolean} Whether there is any overlap
*/
static filter(target: string | PathEvent | (string | PathEvent)[], ...filter: (string | PathEvent)[]): PathEvent[] {
const parsedTarget = makeArray(target).map(pe => new PathEvent(pe));
const parsedFilter = makeArray(filter).map(pe => new PathEvent(pe));
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 => {
const wildcard = r.fullPath == '*' || t.fullPath == '*';
const p1 = r.fullPath.slice(0, r.fullPath.indexOf('*')), p2 = t.fullPath.slice(0, t.fullPath.indexOf('*'))
const p1 = r.fullPath.includes('*') ? r.fullPath.slice(0, r.fullPath.indexOf('*')) : r.fullPath;
const p2 = t.fullPath.includes('*') ? t.fullPath.slice(0, t.fullPath.indexOf('*')) : t.fullPath;
const scope = p1.startsWith(p2) || p2.startsWith(p1);
const methods = r.all || t.all || r.methods.intersection(t.methods).length;
return (wildcard || scope) && methods;
@ -158,12 +179,13 @@ export class PathEvent {
* @return {boolean} Whether there is any overlap
*/
static has(target: string | PathEvent | (string | PathEvent)[], ...has: (string | PathEvent)[]): boolean {
const parsedTarget = makeArray(target).map(pe => new PathEvent(pe));
const parsedRequired = makeArray(has).map(pe => new PathEvent(pe));
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 => {
const wildcard = r.fullPath == '*' || t.fullPath == '*';
const p1 = r.fullPath.slice(0, r.fullPath.indexOf('*')), p2 = t.fullPath.slice(0, t.fullPath.indexOf('*'))
const scope = p1.startsWith(p2);
const p1 = r.fullPath.includes('*') ? r.fullPath.slice(0, r.fullPath.indexOf('*')) : r.fullPath;
const p2 = t.fullPath.includes('*') ? t.fullPath.slice(0, t.fullPath.indexOf('*')) : t.fullPath;
const scope = p1.startsWith(p2); // Note: Original had || p2.startsWith(p1) here, but has implies target has required.
const methods = r.all || t.all || r.methods.intersection(t.methods).length;
return (wildcard || scope) && methods;
}));
@ -293,7 +315,7 @@ export class PathEventEmitter implements IPathEventEmitter{
constructor(public readonly prefix: string = '') { }
emit(event: Event, ...args: any[]) {
const parsed = PE`${this.prefix}/${event}`;
const parsed = event instanceof PathEvent ? event : new PathEvent(`${this.prefix}/${event}`);
this.listeners.filter(l => PathEvent.has(l[0], parsed))
.forEach(async l => l[1](parsed, ...args));
};
@ -304,7 +326,7 @@ export class PathEventEmitter implements IPathEventEmitter{
on(event: Event | Event[], listener: PathListener): PathUnsubscribe {
makeArray(event).forEach(e => this.listeners.push([
new PathEvent(`${this.prefix}/${e}`),
e instanceof PathEvent ? e : new PathEvent(`${this.prefix}/${e}`),
listener
]));
return () => this.off(listener);

View File

@ -10,6 +10,7 @@ describe('Cache', () => {
beforeEach(() => {
storageMock = {
constructor: {name: 'Storage' as any},
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),

View File

@ -1,6 +1,10 @@
import {PathError, PathEvent, PathEventEmitter, PE, PES} from '../src';
describe('Path Events', () => {
beforeEach(() => {
PathEvent.clearCache();
});
describe('PE', () => {
it('creates PathEvent from template string', () => {
const e = PE`users/system:cr`;