diff --git a/package.json b/package.json index f051f59..297c431 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/js-utilities", - "version": "0.3.8", + "version": "0.4.0", "description": "JavaScript Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/logger.ts b/src/logger.ts index a2658a1..bb4def3 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -42,23 +42,23 @@ export const CliBackground = { } export enum LOG_LEVEL { - VERBOSE, - DEBUG = 0, - LOG = 1, + ERROR = 0, + WARN = 1, INFO = 2, - WARN = 3, - ERROR = 4, + LOG = 3, + DEBUG = 4, } export type LoggerEvents = TypedEvents & { - 'VERBOSE': (...args: any[]) => any; - 'INFO': (...args: any[]) => any; - 'WARN': (...args: any[]) => any; 'ERROR': (...args: any[]) => any; + 'WARN': (...args: any[]) => any; + 'INFO': (...args: any[]) => any; + 'LOG': (...args: any[]) => any; + 'DEBUG': (...args: any[]) => any; }; export class Logger extends TypedEmitter { - static LOG_LEVEL: LOG_LEVEL = LOG_LEVEL.VERBOSE; + static LOG_LEVEL: LOG_LEVEL = LOG_LEVEL.DEBUG; constructor(public readonly namespace?: string) { super(); @@ -79,35 +79,35 @@ export class Logger extends TypedEmitter { } debug(...args: string[]) { - if(Logger.LOG_LEVEL > LOG_LEVEL.VERBOSE) return; + if(Logger.LOG_LEVEL < LOG_LEVEL.DEBUG) return; const str = this.format(...args); - Logger.emit(LOG_LEVEL.VERBOSE, str); + Logger.emit(LOG_LEVEL.DEBUG, str); console.debug(CliForeground.LIGHT_GREY + str + CliEffects.CLEAR); } log(...args: string[]) { - if(Logger.LOG_LEVEL > LOG_LEVEL.INFO) return; + if(Logger.LOG_LEVEL < LOG_LEVEL.LOG) return; const str = this.format(...args); - Logger.emit(LOG_LEVEL.INFO, str); + Logger.emit(LOG_LEVEL.LOG, str); console.log(CliEffects.CLEAR + str); } info(...args: string[]) { - if(Logger.LOG_LEVEL > LOG_LEVEL.INFO) return; + if(Logger.LOG_LEVEL < LOG_LEVEL.INFO) return; const str = this.format(...args); Logger.emit(LOG_LEVEL.INFO, str); console.info(CliForeground.BLUE + str + CliEffects.CLEAR); } warn(...args: string[]) { - if(Logger.LOG_LEVEL > LOG_LEVEL.WARN) return; + if(Logger.LOG_LEVEL < LOG_LEVEL.WARN) return; const str = this.format(...args); Logger.emit(LOG_LEVEL.WARN, str); console.warn(CliForeground.YELLOW + str + CliEffects.CLEAR); } error(...args: string[]) { - if(Logger.LOG_LEVEL > LOG_LEVEL.ERROR) return; + if(Logger.LOG_LEVEL < LOG_LEVEL.ERROR) return; const str = this.format(...args); Logger.emit(LOG_LEVEL.ERROR, str); console.error(CliForeground.RED + str + CliEffects.CLEAR); diff --git a/src/xhr.ts b/src/xhr.ts index 733e0cf..9288b4e 100644 --- a/src/xhr.ts +++ b/src/xhr.ts @@ -1,92 +1,88 @@ -export type FetchInterceptor = (resp: Response, next: () => any) => any; +import {TypedEmitter, type TypedEvents} from './emitter'; +import {clean} from './objects'; -export class XHR { - private static interceptors: {[key: number]: FetchInterceptor} = {}; - static headers: Record = {}; +export type Interceptor = (request: Response, next: () => void) => void; - private interceptors: {[key: string]: FetchInterceptor} = {}; +export type RequestOptions = { + url?: string; + method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; + body?: any; + headers?: {[key: string | symbol]: string | null | undefined}; + [key: string]: any; +} - constructor(public readonly baseUrl: string, - public readonly headers: Record = {} - ) { } +export type XhrEvents = TypedEvents & { + 'REQUEST': (request: Promise, options: RequestOptions) => any; + 'RESPONSE': (response: Response, options: RequestOptions) => any; + 'REJECTED': (response: Error, options: RequestOptions) => any; - static addInterceptor(fn: FetchInterceptor): () => {}; - static addInterceptor(key: string, fn: FetchInterceptor): () => {}; - static addInterceptor(keyOrFn: string | FetchInterceptor, fn?: FetchInterceptor): () => {} { - const func: any = fn ? fn : keyOrFn; - const key: string = typeof keyOrFn == 'string' ? keyOrFn : - `_${Object.keys(XHR.interceptors).length.toString()}`; - XHR.interceptors[key] = func; - return () => delete XHR.interceptors[key]; +}; + +export type XhrOptions = { + interceptors?: Interceptor[]; + url?: string; +} + +export class XHR extends TypedEmitter { + private static headers: {[key: string]: string} = {}; + private static interceptors: {[key: string]: Interceptor} = {}; + + private headers: {[key: string]: string} = {} + private interceptors: {[key: string]: Interceptor} = {} + + constructor(public readonly opts: XhrOptions = {}) { + super(); + if(opts.interceptors) { + opts.interceptors.forEach(i => XHR.addInterceptor(i)); + } } - addInterceptor(fn: FetchInterceptor): () => {}; - addInterceptor(key: string, fn: FetchInterceptor): () => {}; - addInterceptor(keyOrFn: string | FetchInterceptor, fn?: FetchInterceptor): () => {} { - const func: any = fn ? fn : keyOrFn; - const key: string = typeof keyOrFn == 'string' ? keyOrFn : - `_${Object.keys(this.interceptors).length.toString()}`; - this.interceptors[key] = func; - return () => delete this.interceptors[key]; + static addInterceptor(fn: Interceptor): () => void { + const key = Object.keys(XHR.interceptors).length.toString(); + XHR.interceptors[key] = fn; + return () => { XHR.interceptors[key] = null; } } - getInterceptors() { - return [...Object.values(XHR.interceptors), ...Object.values(this.interceptors)]; + addInterceptor(fn: Interceptor): () => void { + const key = Object.keys(this.interceptors).length.toString(); + this.interceptors[key] = fn; + return () => { this.interceptors[key] = null; } } - fetch(href?: string, body?: any, opts: any = {}): Promise { - const headers = { - 'Content-Type': (body && !(body instanceof FormData)) ? 'application/json' : undefined, + async request(opts: RequestOptions = {}): Promise { + if(!this.opts.url && !opts.url) throw new Error('Momentum server URL needs to be set'); + const url = (opts.url?.startsWith('http') ? opts.url : (this.opts.url || '') + opts.url).replace(/([^:]\/)\/+/g, '$1'); + + // Prep headers + const headers = clean({ + 'Content-Type': (opts.body && !(opts.body instanceof FormData)) ? 'application/json' : undefined, ...XHR.headers, ...this.headers, ...opts.headers - }; - Object.keys(headers).forEach(h => { if(!headers[h]) delete headers[h]; }); - return fetch(`${this.baseUrl}${href || ''}`.replace(/([^:]\/)\/+/g, '$1'), { + }); + + // Send request + const req = fetch(url, { headers, - method: opts.method || (body ? 'POST' : 'GET'), - body: (headers['Content-Type']?.startsWith('application/json') && body) ? JSON.stringify(body) : body + method: opts.method || (opts.body ? 'POST' : 'GET'), + body: (headers['Content-Type']?.startsWith('application/json') && opts.body) ? JSON.stringify(opts.body) : opts.body }).then(async resp => { - for(let fn of this.getInterceptors()) { - const wait = new Promise(res => - fn(resp, () => res(null))); + for(let fn of [...Object.values(XHR.interceptors), ...Object.values(this.interceptors)]) { + const wait = new Promise(res => fn(resp, () => res(null))); await wait; } - if(resp.headers.has('Content-Type')) { - if(resp.headers.get('Content-Type')?.startsWith('application/json')) return await resp.json(); - if(resp.headers.get('Content-Type')?.startsWith('text/plain')) return await resp.text(); - } + + this.emit(`${resp.status}`, resp, opts); + if(!resp.ok) throw Error(resp.statusText); + this.emit('RESPONSE', resp, opts); + if(resp.headers.get('Content-Type')?.startsWith('application/json')) return await resp.json(); + if(resp.headers.get('Content-Type')?.startsWith('text/plain')) return await resp.text(); return resp; + }).catch((err: Error) => { + this.emit('REJECTED', err, opts); + throw err; }); - } - - delete(url?: string, opts?: any): Promise { - return this.fetch(url, null, {method: 'delete', ...opts}); - } - - get(url?: string, opts?: any): Promise { - return this.fetch(url, null, {method: 'get', ...opts}); - } - - patch(data: T2, url?: string, opts?: any): Promise { - return this.fetch(url, data, {method: 'patch', ...opts}); - } - - post(data: T2, url?: string, opts?: any): Promise { - return this.fetch(url, data, {method: 'post', ...opts}); - } - - put(data: Partial, url?: string, opts?: any): Promise { - return this.fetch(url, data, {method: 'put', ...opts}); - } - - new(href: string, headers: Record): XHR { - const fetch = new XHR(`${this.baseUrl}${href}`, { - ...this.headers, - ...headers, - }); - Object.entries(this.interceptors).map(([key, value]) => - fetch.addInterceptor(key, value)); - return fetch; + this.emit('REQUEST', req, opts) + return req; } }