Compare commits

...

15 Commits

Author SHA1 Message Date
7c5cf3535d Handle logging objects
All checks were successful
Build / Build NPM Project (push) Successful in 39s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 35s
2024-10-16 20:21:35 -04:00
847b493772 Handle logging objects
All checks were successful
Build / Build NPM Project (push) Successful in 38s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 34s
2024-10-16 19:55:35 -04:00
b1005227ab Added PathError for easy detection
All checks were successful
Build / Build NPM Project (push) Successful in 43s
Build / Tag Version (push) Successful in 8s
Build / Publish Documentation (push) Successful in 38s
2024-10-16 19:52:47 -04:00
3a389de72e Cache localstorage fix
All checks were successful
Build / Build NPM Project (push) Successful in 38s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 35s
2024-10-15 16:44:23 -04:00
151465aa65 Cache localstorage fix
All checks were successful
Build / Build NPM Project (push) Successful in 39s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 36s
2024-10-15 16:39:29 -04:00
b103b6f786 Cache localstorage fix
All checks were successful
Build / Build NPM Project (push) Successful in 38s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 34s
2024-10-15 16:35:11 -04:00
3b486310de Deleting lock file, seems to mess up build
All checks were successful
Build / Build NPM Project (push) Successful in 1m4s
Build / Tag Version (push) Successful in 11s
Build / Publish Documentation (push) Successful in 46s
2024-10-15 13:51:00 -04:00
8699fb49ff New lock file
Some checks failed
Build / Build NPM Project (push) Failing after 18s
Build / Tag Version (push) Has been skipped
Build / Publish Documentation (push) Has been skipped
2024-10-15 13:49:52 -04:00
fdb29e7984 New lock file
Some checks failed
Build / Tag Version (push) Blocked by required conditions
Build / Publish Documentation (push) Blocked by required conditions
Build / Build NPM Project (push) Has been cancelled
2024-10-15 13:49:17 -04:00
274c22bb83 Fixed localStorage access on node environments
Some checks failed
Build / Build NPM Project (push) Failing after 15s
Build / Tag Version (push) Has been skipped
Build / Publish Documentation (push) Has been skipped
2024-10-15 13:46:11 -04:00
b21f462d35 Added clear function to cache
All checks were successful
Build / Build NPM Project (push) Successful in 37s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 35s
2024-10-14 21:07:59 -04:00
0f10aebfd2 bubble up all fetch errors in the http helper
All checks were successful
Build / Build NPM Project (push) Successful in 40s
Build / Tag Version (push) Successful in 8s
Build / Publish Documentation (push) Successful in 36s
2024-10-14 20:46:32 -04:00
1af23ac544 Fixed cache localstorage
All checks were successful
Build / Build NPM Project (push) Successful in 37s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 34s
2024-10-14 20:04:29 -04:00
494cfaaccd Added localStorage support to cache
All checks were successful
Build / Build NPM Project (push) Successful in 41s
Build / Tag Version (push) Successful in 8s
Build / Publish Documentation (push) Successful in 41s
2024-10-14 18:51:24 -04:00
23df6ad265 Renamed PathEventEmitter to follow convention
All checks were successful
Build / Build NPM Project (push) Successful in 27s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 27s
2024-10-14 14:09:51 -04:00
7 changed files with 114 additions and 5834 deletions

5753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@ztimson/utils", "name": "@ztimson/utils",
"version": "0.19.1", "version": "0.20.10",
"description": "Utility library", "description": "Utility library",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",
@ -38,8 +38,5 @@
}, },
"files": [ "files": [
"dist" "dist"
], ]
"dependencies": {
"var-persist": "^1.0.1"
}
} }

View File

@ -1,3 +1,12 @@
export type CacheOptions = {
/** Delete keys automatically after x amount of seconds */
ttl?: number;
/** Storage to persist cache */
storage?: Storage;
/** Key cache will be stored under */
storageKey?: string;
}
/** /**
* Map of data which tracks whether it is a complete collection & offers optional expiry of cached values * Map of data which tracks whether it is a complete collection & offers optional expiry of cached values
*/ */
@ -13,9 +22,18 @@ export class Cache<K extends string | number | symbol, T> {
* Create new cache * Create new cache
* *
* @param {keyof T} key Default property to use as primary key * @param {keyof T} key Default property to use as primary key
* @param {number} ttl Default expiry in milliseconds * @param options
*/ */
constructor(public readonly key?: keyof T, public ttl?: number) { constructor(public readonly key?: keyof T, public readonly options: CacheOptions = {}) {
if(options.storageKey && !options.storage && typeof(Storage) !== 'undefined')
options.storage = localStorage;
if(options.storageKey && options.storage) {
const stored = options.storage.getItem(options.storageKey);
if(stored) {
try { Object.assign(this.store, JSON.parse(stored)); }
catch { }
}
}
return new Proxy(this, { return new Proxy(this, {
get: (target: this, prop: string | symbol) => { get: (target: this, prop: string | symbol) => {
if (prop in target) return (target as any)[prop]; if (prop in target) return (target as any)[prop];
@ -69,6 +87,13 @@ export class Cache<K extends string | number | symbol, T> {
return this; return this;
} }
/**
* Remove all keys from cache
*/
clear() {
this.store = <Record<K, T>>{};
}
/** /**
* Delete an item from the cache * Delete an item from the cache
* *
@ -76,6 +101,8 @@ export class Cache<K extends string | number | symbol, T> {
*/ */
delete(key: K) { delete(key: K) {
delete this.store[key]; delete this.store[key];
if(this.options.storageKey && this.options.storage)
this.options.storage.setItem(this.options.storageKey, JSON.stringify(this.store));
} }
/** /**
@ -118,15 +145,17 @@ export class Cache<K extends string | number | symbol, T> {
* *
* @param {K} key Key item will be cached under * @param {K} key Key item will be cached under
* @param {T} value Item to cache * @param {T} value Item to cache
* @param {number | undefined} ttl Override default expiry * @param {number | undefined} ttl Override default expiry in seconds
* @return {this} * @return {this}
*/ */
set(key: K, value: T, ttl = this.ttl): this { set(key: K, value: T, ttl = this.options.ttl): this {
this.store[key] = value; this.store[key] = value;
if(this.options.storageKey && this.options.storage)
this.options.storage.setItem(this.options.storageKey, JSON.stringify(this.store));
if(ttl) setTimeout(() => { if(ttl) setTimeout(() => {
this.complete = false; this.complete = false;
this.delete(key); this.delete(key);
}, ttl); }, ttl * 1000);
return this; return this;
} }

View File

@ -75,47 +75,52 @@ export class Http {
// Send request // Send request
return new PromiseProgress((res, rej, prog) => { return new PromiseProgress((res, rej, prog) => {
fetch(url, { try {
headers, fetch(url, {
method: opts.method || (opts.body ? 'POST' : 'GET'), headers,
body: opts.body method: opts.method || (opts.body ? 'POST' : 'GET'),
}).then(async (resp: any) => { body: opts.body
for(let fn of [...Object.values(Http.interceptors), ...Object.values(this.interceptors)]) { }).then(async (resp: any) => {
await new Promise<void>(res => fn(resp, () => res())); for(let fn of [...Object.values(Http.interceptors), ...Object.values(this.interceptors)]) {
} await new Promise<void>(res => fn(resp, () => res()));
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: any) => {
if(event.done) return controller.close();
loaded += event.value.byteLength;
prog(loaded / total);
controller.enqueue(event.value);
push();
}).catch((error: any) => controller.error(error));
}
push();
} }
});
resp.data = new Response(stream); const contentLength = resp.headers.get('Content-Length');
if(opts.decode == null || opts.decode) { const total = contentLength ? parseInt(contentLength, 10) : 0;
const content = resp.headers.get('Content-Type')?.toLowerCase(); let loaded = 0;
if(content?.includes('form')) resp.data = <T>await resp.data.formData();
else if(content?.includes('json')) resp.data = <T>await resp.data.json();
else if(content?.includes('text')) resp.data = <T>await resp.data.text();
else if(content?.includes('application')) resp.data = <T>await resp.data.blob();
}
if(resp.ok) res(resp); const reader = resp.body?.getReader();
else rej(resp); const stream = new ReadableStream({
}) start(controller) {
function push() {
reader?.read().then((event: any) => {
if(event.done) return controller.close();
loaded += event.value.byteLength;
prog(loaded / total);
controller.enqueue(event.value);
push();
}).catch((error: any) => controller.error(error));
}
push();
}
});
resp.data = new Response(stream);
if(opts.decode == null || opts.decode) {
const content = resp.headers.get('Content-Type')?.toLowerCase();
if(content?.includes('form')) resp.data = <T>await resp.data.formData();
else if(content?.includes('json')) resp.data = <T>await resp.data.json();
else if(content?.includes('text')) resp.data = <T>await resp.data.text();
else if(content?.includes('application')) resp.data = <T>await resp.data.blob();
}
if(resp.ok) res(resp);
else rej(resp);
}).catch(err => rej(err));
} catch(err) {
rej(err);
}
}); });
} }
} }

View File

@ -16,4 +16,3 @@ export * from './promise-progress';
export * from './string'; export * from './string';
export * from './time'; export * from './time';
export * from './types'; export * from './types';
export * from 'var-persist';

View File

@ -1,4 +1,5 @@
import {TypedEmitter, TypedEvents} from './emitter'; import {TypedEmitter, TypedEvents} from './emitter';
import {JSONSanitize} from './objects.ts';
export const CliEffects = { export const CliEffects = {
CLEAR: "\x1b[0m", CLEAR: "\x1b[0m",
@ -72,41 +73,41 @@ export class Logger extends TypedEmitter<LoggerEvents> {
return !end ? padding + t : t + padding; return !end ? padding + t : t + padding;
} }
private format(...text: string[]): string { private format(...text: any[]): string {
const now = new Date(); const now = new Date();
const timestamp = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${this.pad(now.getHours().toString(), 2, '0')}:${this.pad(now.getMinutes().toString(), 2, '0')}:${this.pad(now.getSeconds().toString(), 2, '0')}.${this.pad(now.getMilliseconds().toString(), 3, '0', true)}`; const timestamp = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${this.pad(now.getHours().toString(), 2, '0')}:${this.pad(now.getMinutes().toString(), 2, '0')}:${this.pad(now.getSeconds().toString(), 2, '0')}.${this.pad(now.getMilliseconds().toString(), 3, '0', true)}`;
return `${timestamp}${this.namespace ? ` [${this.namespace}]` : ''} ${text.join(' ')}`; return `${timestamp}${this.namespace ? ` [${this.namespace}]` : ''} ${text.map(JSONSanitize).join(' ')}`;
} }
debug(...args: string[]) { debug(...args: any[]) {
if(Logger.LOG_LEVEL < LOG_LEVEL.DEBUG) return; if(Logger.LOG_LEVEL < LOG_LEVEL.DEBUG) return;
const str = this.format(...args); const str = this.format(...args);
Logger.emit(LOG_LEVEL.DEBUG, str); Logger.emit(LOG_LEVEL.DEBUG, str);
console.debug(CliForeground.LIGHT_GREY + str + CliEffects.CLEAR); console.debug(CliForeground.LIGHT_GREY + str + CliEffects.CLEAR);
} }
log(...args: string[]) { log(...args: any[]) {
if(Logger.LOG_LEVEL < LOG_LEVEL.LOG) return; if(Logger.LOG_LEVEL < LOG_LEVEL.LOG) return;
const str = this.format(...args); const str = this.format(...args);
Logger.emit(LOG_LEVEL.LOG, str); Logger.emit(LOG_LEVEL.LOG, str);
console.log(CliEffects.CLEAR + str); console.log(CliEffects.CLEAR + str);
} }
info(...args: string[]) { info(...args: any[]) {
if(Logger.LOG_LEVEL < LOG_LEVEL.INFO) return; if(Logger.LOG_LEVEL < LOG_LEVEL.INFO) return;
const str = this.format(...args); const str = this.format(...args);
Logger.emit(LOG_LEVEL.INFO, str); Logger.emit(LOG_LEVEL.INFO, str);
console.info(CliForeground.BLUE + str + CliEffects.CLEAR); console.info(CliForeground.BLUE + str + CliEffects.CLEAR);
} }
warn(...args: string[]) { warn(...args: any[]) {
if(Logger.LOG_LEVEL < LOG_LEVEL.WARN) return; if(Logger.LOG_LEVEL < LOG_LEVEL.WARN) return;
const str = this.format(...args); const str = this.format(...args);
Logger.emit(LOG_LEVEL.WARN, str); Logger.emit(LOG_LEVEL.WARN, str);
console.warn(CliForeground.YELLOW + str + CliEffects.CLEAR); console.warn(CliForeground.YELLOW + str + CliEffects.CLEAR);
} }
error(...args: string[]) { error(...args: any[]) {
if(Logger.LOG_LEVEL < LOG_LEVEL.ERROR) return; if(Logger.LOG_LEVEL < LOG_LEVEL.ERROR) return;
const str = this.format(...args); const str = this.format(...args);
Logger.emit(LOG_LEVEL.ERROR, str); Logger.emit(LOG_LEVEL.ERROR, str);

View File

@ -14,16 +14,16 @@ import {ASet} from './aset.ts';
export type Method = '*' | 'n' | 'c' | 'r' | 'u' | 'd' | 'x'; export type Method = '*' | 'n' | 'c' | 'r' | 'u' | 'd' | 'x';
/** /**
* Shorthand for creating PathedEvent from a string * Shorthand for creating Event from a string
* *
* @example * @example
* ```ts * ```ts
* const event: PathedEvent = PE`users/system:*`; * const event: Event = PE`users/system:*`;
* ``` * ```
* *
* @param {TemplateStringsArray} str String that will be parsed into PathedEvent * @param {TemplateStringsArray} str String that will be parsed into Event
* @param {string} args * @param {string} args
* @return {PathEvent} PathedEvent object * @return {PathEvent} Event object
*/ */
export function PE(str: TemplateStringsArray, ...args: string[]) { export function PE(str: TemplateStringsArray, ...args: string[]) {
const combined = []; const combined = [];
@ -35,7 +35,7 @@ export function PE(str: TemplateStringsArray, ...args: string[]) {
} }
/** /**
* Shorthand for creating PathedEvent strings, ensures paths are correct * Shorthand for creating Event strings, ensures paths are correct
* *
* @param {TemplateStringsArray} str * @param {TemplateStringsArray} str
* @param {string} args * @param {string} args
@ -52,9 +52,11 @@ export function PES(str: TemplateStringsArray, ...args: any[]) {
return PathEvent.toString(paths, <any>methods?.split('')); return PathEvent.toString(paths, <any>methods?.split(''));
} }
export class PathError extends Error { }
/** /**
* A pathed event broken down into its core components for easy processing * A event broken down into its core components for easy processing
* PathedEvent Structure: `module/path/name:property:method` * Event Structure: `module/path/name:property:method`
* Example: `users/system:crud` or `storage/some/path/file.txt:r` * Example: `users/system:crud` or `storage/some/path/file.txt:r`
*/ */
export class PathEvent { export class PathEvent {
@ -81,9 +83,9 @@ export class PathEvent {
/** Delete method specified */ /** Delete method specified */
delete!: boolean; delete!: boolean;
constructor(pathedEvent: string | PathEvent) { constructor(Event: string | PathEvent) {
if(typeof pathedEvent == 'object') return Object.assign(this, pathedEvent); if(typeof Event == 'object') return Object.assign(this, Event);
let [p, scope, method] = pathedEvent.split(':'); let [p, scope, method] = Event.split(':');
if(!method) method = scope || '*'; if(!method) method = scope || '*';
if(p == '*' || !p && method == '*') { if(p == '*' || !p && method == '*') {
p = ''; p = '';
@ -104,10 +106,10 @@ export class PathEvent {
} }
/** /**
* Combine multiple pathed events into one parsed object. Longest path takes precedent, but all subsequent methods are * Combine multiple events into one parsed object. Longest path takes precedent, but all subsequent methods are
* combined until a "none" is reached * combined until a "none" is reached
* *
* @param {string | PathEvent} paths PathedEvents as strings or pre-parsed * @param {string | PathEvent} paths Events as strings or pre-parsed
* @return {PathEvent} Final combined permission * @return {PathEvent} Final combined permission
*/ */
static combine(paths: (string | PathEvent)[]): PathEvent { static combine(paths: (string | PathEvent)[]): PathEvent {
@ -138,7 +140,7 @@ export class PathEvent {
/** /**
* Squash 2 sets of paths & return true if any overlap is found * Squash 2 sets of paths & return true if any overlap is found
* *
* @param {string | PathEvent | (string | PathEvent)[]} target Array of PathedEvents as strings or pre-parsed * @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 * @param has Target must have at least one of these path
* @return {boolean} Whether there is any overlap * @return {boolean} Whether there is any overlap
*/ */
@ -157,7 +159,7 @@ export class PathEvent {
/** /**
* Squash 2 sets of paths & return true if the target has all paths * Squash 2 sets of paths & return true if the target has all paths
* *
* @param {string | PathEvent | (string | PathEvent)[]} target Array of PathedEvents as strings or pre-parsed * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
* @param has Target must have all these paths * @param has Target must have all these paths
* @return {boolean} Whether there is any overlap * @return {boolean} Whether there is any overlap
*/ */
@ -168,29 +170,29 @@ export class PathEvent {
/** /**
* Same as `has` but raises an error if there is no overlap * Same as `has` but raises an error if there is no overlap
* *
* @param {string | string[]} target Array of PathedEvents as strings or pre-parsed * @param {string | string[]} target Array of Events as strings or pre-parsed
* @param has Target must have at least one of these path * @param has Target must have at least one of these path
*/ */
static hasFatal(target: string | PathEvent | (string | PathEvent)[], ...has: (string | PathEvent)[]): void { static hasFatal(target: string | PathEvent | (string | PathEvent)[], ...has: (string | PathEvent)[]): void {
if(!PathEvent.has(target, ...has)) throw new Error(`Requires one of: ${makeArray(has).join(', ')}`); 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 * Same as `hasAll` but raises an error if the target is missing any paths
* *
* @param {string | string[]} target Array of PathedEvents as strings or pre-parsed * @param {string | string[]} target Array of Events as strings or pre-parsed
* @param has Target must have all these paths * @param has Target must have all these paths
*/ */
static hasAllFatal(target: string | PathEvent | (string | PathEvent)[], ...has: (string | PathEvent)[]): void { static hasAllFatal(target: string | PathEvent | (string | PathEvent)[], ...has: (string | PathEvent)[]): void {
if(!PathEvent.hasAll(target, ...has)) throw new Error(`Requires all: ${makeArray(has).join(', ')}`); if(!PathEvent.hasAll(target, ...has)) throw new PathError(`Requires all: ${makeArray(has).join(', ')}`);
} }
/** /**
* Create pathed event string from its components * Create event string from its components
* *
* @param {string | string[]} path Event path * @param {string | string[]} path Event path
* @param {Method} methods Event method * @param {Method} methods Event method
* @return {string} String representation of PathedEvent * @return {string} String representation of Event
*/ */
static toString(path: string | string[], methods: Method | Method[]): string { static toString(path: string | string[], methods: Method | Method[]): string {
let p = makeArray(path).filter(p => p != null).join('/'); let p = makeArray(path).filter(p => p != null).join('/');
@ -199,9 +201,9 @@ export class PathEvent {
} }
/** /**
* Create pathed event string from its components * Create event string from its components
* *
* @return {string} String representation of PathedEvent * @return {string} String representation of Event
*/ */
toString() { toString() {
return PathEvent.toString(this.fullPath, this.methods); return PathEvent.toString(this.fullPath, this.methods);
@ -211,7 +213,7 @@ export class PathEvent {
export type PathListener = (event: PathEvent, ...args: any[]) => any; export type PathListener = (event: PathEvent, ...args: any[]) => any;
export type PathUnsubscribe = () => void; export type PathUnsubscribe = () => void;
export interface IPathedEventEmitter { export interface IPathEventEmitter {
emit(event: string, ...args: any[]): void; emit(event: string, ...args: any[]): void;
off(listener: PathListener): void; off(listener: PathListener): void;
on(event: string, listener: PathListener): PathUnsubscribe; on(event: string, listener: PathListener): PathUnsubscribe;
@ -222,7 +224,7 @@ export interface IPathedEventEmitter {
/** /**
* Event emitter that uses paths allowing listeners to listen to different combinations of modules, paths & methods * Event emitter that uses paths allowing listeners to listen to different combinations of modules, paths & methods
*/ */
export class PathEventEmitter implements IPathedEventEmitter{ export class PathEventEmitter implements IPathEventEmitter{
private listeners: [PathEvent, PathListener][] = []; private listeners: [PathEvent, PathListener][] = [];
emit(event: string | PathEvent, ...args: any[]) { emit(event: string | PathEvent, ...args: any[]) {
@ -250,7 +252,7 @@ export class PathEventEmitter implements IPathedEventEmitter{
}); });
} }
relayEvents(emitter: IPathedEventEmitter) { relayEvents(emitter: IPathEventEmitter) {
emitter.on('*', (event, ...args) => this.emit(event, ...args)); emitter.on('*', (event, ...args) => this.emit(event, ...args));
} }
} }