diff --git a/.eslintrc.js b/.eslintrc.js index 59bfea1e..85b2274f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 0, 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', + 'prefer-const': ['error', { destructuring: 'all'}], }, }; diff --git a/packages/tools/package.json b/packages/tools/package.json index 0994c0e6..af567db0 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -56,6 +56,7 @@ "dependencies": { "bignumber.js": "^9.1.1", "dayjs": "^1.11.7", + "loglevel": "^1.8.1", "tslib": "^2.5.0" }, "typedoc": { diff --git a/packages/tools/src/const/event-bus.ts b/packages/tools/src/const/event-bus.ts new file mode 100644 index 00000000..c935bdfc --- /dev/null +++ b/packages/tools/src/const/event-bus.ts @@ -0,0 +1,6 @@ +export const EVENT_BUS_EVENTS = { + warning: 'warning', + success: 'success', + error: 'error', + info: 'info', +} as const diff --git a/packages/tools/src/const/index.ts b/packages/tools/src/const/index.ts index 5441e611..af5bf064 100644 --- a/packages/tools/src/const/index.ts +++ b/packages/tools/src/const/index.ts @@ -1 +1,2 @@ export * from './bn' +export * from './event-bus' diff --git a/packages/tools/src/errors.ts b/packages/tools/src/errors.ts deleted file mode 100644 index 9d61d1fe..00000000 --- a/packages/tools/src/errors.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class RuntimeError extends Error { - public name = 'RuntimeError' - public originalError?: Error - - public constructor(errorOrMessage: Error | string) - public constructor(message: string, error: Error) - - public constructor(errorOrMessage: Error | string, error?: Error) { - if (error) { - super(errorOrMessage as string) - - this.originalError = error - } else { - if (typeof errorOrMessage === 'string') { - super(errorOrMessage) - } else { - super(errorOrMessage?.message) - - this.originalError = errorOrMessage - } - } - } -} diff --git a/packages/tools/src/errors/index.ts b/packages/tools/src/errors/index.ts new file mode 100644 index 00000000..283161a4 --- /dev/null +++ b/packages/tools/src/errors/index.ts @@ -0,0 +1,2 @@ +export * from './problem' +export * from './runtime-error' diff --git a/packages/tools/src/errors/problem.ts b/packages/tools/src/errors/problem.ts new file mode 100644 index 00000000..bc34752e --- /dev/null +++ b/packages/tools/src/errors/problem.ts @@ -0,0 +1,77 @@ +import log from 'loglevel' + +import { EventBus } from '@/events' +import type { ProblemConfig } from '@/types' + +import { RuntimeError } from './runtime-error' + +let config: ProblemConfig = { + eventBus: new EventBus(), +} + +export class Problem { + /** + * `setConfig` overrides default config. + */ + public static setConfig(cfg: ProblemConfig): void { + config = { ...config, ...cfg } + } + + /** + * `new` returns an error with the supplied message. + */ + public static new(message: string): RuntimeError { + return new RuntimeError(message) + } + + /** + * `wrap` returns an error annotating err with a stack trace + * at the point wrap is called, and the supplied message. + * + * + * Fields can optionally be added. If provided, multiple fields will be merged. + * + * If err is null, Wrap returns null. + */ + public static wrap( + error: Error | RuntimeError | null | undefined, + message: string, + ...errorFields: object[] + ): RuntimeError | null { + return error ? new RuntimeError(message, error, ...errorFields) : null + } + + /** + * `cause` returns the underlying cause of the error, if possible. + * If the error is null, null will be returned without further + * investigation. + */ + public static cause(error?: Error | null): Error | null { + if (!error) return null + if (!Problem.isRuntimeError(error)) return error + return error.originalError ? Problem.cause(error.originalError) : error + } + + public static isRuntimeError(error?: Error): error is RuntimeError { + return error instanceof RuntimeError + } + + /** + * `handle` handles provided error, error could be logged by default, and + * emitted via event bus if `eventBus` is set in the config. + * + * optional message parameter will be emitted with the error if presented + */ + public static handle(error: unknown, message?: string): void { + if (!(error instanceof Error)) return + config?.eventBus?.error?.({ error, message }) + Problem.handleWithoutFeedback(error) + } + + /** + * `handleWithoutFeedback` logs provided an error without emitting it via event bus. + */ + public static handleWithoutFeedback(error: Error | RuntimeError): void { + log.error(error) + } +} diff --git a/packages/tools/src/errors/runtime-error.ts b/packages/tools/src/errors/runtime-error.ts new file mode 100644 index 00000000..4d26d665 --- /dev/null +++ b/packages/tools/src/errors/runtime-error.ts @@ -0,0 +1,69 @@ +import { Problem } from './problem' + +export class RuntimeError extends Error { + public name = 'RuntimeError' + public originalError?: Error + public errorFields: object[] = [] + + public constructor(errorOrMessage: Error | string) + public constructor(message: string, error: Error) + public constructor(message: string, error: Error, ...errorFields: object[]) + public constructor( + errorOrMessage: Error | string, + error?: Error, + ...errorFields: object[] + ) { + const isErrorOrMessageString = typeof errorOrMessage === 'string' + const message = isErrorOrMessageString + ? errorOrMessage + : errorOrMessage?.message + + super(message) + + this.originalError = + error || isErrorOrMessageString ? undefined : errorOrMessage + + this.errorFields = errorFields + } + + public toString() { + let name = this.name + + const originalError = Problem.isRuntimeError(this.originalError) + ? Problem.cause(this.originalError) + : this.originalError + + if (originalError && !Problem.isRuntimeError(originalError)) { + name = `${name}: ${originalError.name}` + } + + let message = this.message + let err = this.originalError + const fields = [...this.errorFields] + + for (;;) { + if (!err) break + + if (!Problem.isRuntimeError(err)) { + message += `: ${err.message}` + break + } + + err = err.originalError + fields.push(...(err as RuntimeError).errorFields) + message += ` :${err!.message}` + } + + const fieldObj = fields.reduce((acc, field) => { + acc = { ...acc, ...field } + return acc + }, {}) + + message = Object.entries(fieldObj).reduce((acc, [key, value]) => { + acc += ` ${key}: ${JSON.stringify(value)}` + return acc + }, message) + + return `${name}: ${message}` + } +} diff --git a/packages/tools/src/events/event-bus.ts b/packages/tools/src/events/event-bus.ts new file mode 100644 index 00000000..122b70f6 --- /dev/null +++ b/packages/tools/src/events/event-bus.ts @@ -0,0 +1,102 @@ +import log from 'loglevel' + +import { EVENT_BUS_EVENTS } from '@/const' +import type { + EventBusEvent, + EventBusEventEmitterEventMap, + EventBusEventHandler, + EventBusEventMap, + EventBusEventName, + EventHandler, +} from '@/types' + +import { EventEmitter } from './event-emitter' + +export class EventBus { + readonly #events: EventBusEventMap + readonly #emitter: EventEmitter< + EventBusEventEmitterEventMap + > + #backlog: EventBusEvent>[] + + constructor(events?: AdditionalEventBusMap) { + this.#backlog = [] + this.#emitter = new EventEmitter< + EventBusEventEmitterEventMap + >() + + this.#events = { + ...EVENT_BUS_EVENTS, + ...(events || {}), + } as EventBusEventMap + } + + public get events(): EventBusEventMap { + return this.#events + } + + public isEventExists( + event: EventBusEventName, + ): boolean { + const values = Object.values( + this.#events, + ) as EventBusEventName[] + return values.includes(event) + } + + public on( + event: EventBusEventName>, + handler: EventBusEventHandler, + ): void { + if (!this.isEventExists(event)) { + throw new Error(`EventBus.list has no ${event} event`) + } + + const backloggedEvents = this.#backlog.filter(e => e.name === event) + + for (const [index, eventObj] of backloggedEvents.entries()) { + handler(eventObj.payload as Payload) + this.#backlog.splice(index, 1) + log.debug(`Event ${event} is backlogged. Handling...`) + } + this.#emitter.on(event, handler as EventHandler) + } + + public emit( + event: EventBusEventName>, + payload?: Payload, + ): void { + if (!this.isEventExists(event)) { + throw new Error(`EventBus.list has no ${event.toString()} event`) + } + + this.#emitter.emit(event, payload) + } + + public reset( + event: EventBusEventName>, + handler: EventBusEventHandler, + ): void { + if (!this.isEventExists(event)) { + throw new Error(`EventBus.list has no ${event.toString()} event`) + } + this.#emitter.off(event, handler as EventHandler) + this.#backlog = [] + } + + public success(payload: Payload): void { + this.#emitter.emit(this.#events.success, payload) + } + + public warning(payload: Payload): void { + this.#emitter.emit(this.#events.warning, payload) + } + + public error(payload: Payload): void { + this.#emitter.emit(this.#events.error, payload) + } + + public info(payload: Payload): void { + this.#emitter.emit(this.#events.info, payload) + } +} diff --git a/packages/tools/src/event-emitter.test.ts b/packages/tools/src/events/event-emitter.test.ts similarity index 100% rename from packages/tools/src/event-emitter.test.ts rename to packages/tools/src/events/event-emitter.test.ts diff --git a/packages/tools/src/event-emitter.ts b/packages/tools/src/events/event-emitter.ts similarity index 100% rename from packages/tools/src/event-emitter.ts rename to packages/tools/src/events/event-emitter.ts diff --git a/packages/tools/src/events/index.ts b/packages/tools/src/events/index.ts new file mode 100644 index 00000000..5a968d0c --- /dev/null +++ b/packages/tools/src/events/index.ts @@ -0,0 +1,2 @@ +export * from './event-bus' +export * from './event-emitter' diff --git a/packages/tools/src/go.test.ts b/packages/tools/src/go.test.ts new file mode 100644 index 00000000..126c264b --- /dev/null +++ b/packages/tools/src/go.test.ts @@ -0,0 +1,53 @@ +import { go } from './go' + +describe('preforms go unit test', () => { + describe('async callback should return', () => { + test('null result if error thrown', async () => { + const [err, result] = await go(async () => { + throw new Error('test error') + }) + + expect(err).toBeInstanceOf(Error) + expect(result).toBeNull() + }) + + test('result if no error thrown', async () => { + const [err, result] = await go(async () => { + return 'test result' + }) + expect(err).toBeNull() + expect(result).toBe('test result') + }) + }) + + describe('sync callback should return', () => { + test('null result if error thrown', async () => { + const [err, result] = await go(() => { + throw new Error('test error') + }) + expect(err).toBeInstanceOf(Error) + expect(result).toBeNull() + }) + + test('result if no error thrown', async () => { + const [err, result] = await go(() => { + return 'test result' + }) + expect(err).toBeNull() + expect(result).toBe('test result') + }) + }) + + test('should redeclare error during few executions', async () => { + let [err, result] = await go(async () => { + throw new Error('test error') + }) + + expect(err).toBeInstanceOf(Error) + expect(result).toBeNull() + ;[err] = await go(async () => { + return 'success' + }) + expect(err).toBeNull() + }) +}) diff --git a/packages/tools/src/go.ts b/packages/tools/src/go.ts new file mode 100644 index 00000000..bd1c7159 --- /dev/null +++ b/packages/tools/src/go.ts @@ -0,0 +1,10 @@ +export const go = async unknown, E = Error>( + cb: C, +): Promise<[E | null, Awaited> | null]> => { + try { + const res = await cb() + return [null, res as Awaited>] + } catch (e) { + return [e as E, null] + } +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 31bd08d6..29ac8ad4 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,8 +1,8 @@ -export * from '@/bn' -export * from '@/duration' export * from '@/enums' export * from '@/errors' -export * from '@/event-emitter' +export * from '@/events' +export * from '@/go' export * from '@/helpers' +export * from '@/math' export * from '@/time' export * from '@/types' diff --git a/packages/tools/src/bn/assertions.ts b/packages/tools/src/math/assertions.ts similarity index 100% rename from packages/tools/src/bn/assertions.ts rename to packages/tools/src/math/assertions.ts diff --git a/packages/tools/src/bn/bn.test.ts b/packages/tools/src/math/bn.test.ts similarity index 100% rename from packages/tools/src/bn/bn.test.ts rename to packages/tools/src/math/bn.test.ts diff --git a/packages/tools/src/bn/bn.ts b/packages/tools/src/math/bn.ts similarity index 100% rename from packages/tools/src/bn/bn.ts rename to packages/tools/src/math/bn.ts diff --git a/packages/tools/src/bn/decimals.ts b/packages/tools/src/math/decimals.ts similarity index 100% rename from packages/tools/src/bn/decimals.ts rename to packages/tools/src/math/decimals.ts diff --git a/packages/tools/src/bn/format.ts b/packages/tools/src/math/format.ts similarity index 100% rename from packages/tools/src/bn/format.ts rename to packages/tools/src/math/format.ts diff --git a/packages/tools/src/bn/index.ts b/packages/tools/src/math/index.ts similarity index 100% rename from packages/tools/src/bn/index.ts rename to packages/tools/src/math/index.ts diff --git a/packages/tools/src/bn/parsers.ts b/packages/tools/src/math/parsers.ts similarity index 100% rename from packages/tools/src/bn/parsers.ts rename to packages/tools/src/math/parsers.ts diff --git a/packages/tools/src/bn/round.ts b/packages/tools/src/math/round.ts similarity index 96% rename from packages/tools/src/bn/round.ts rename to packages/tools/src/math/round.ts index ad83d747..b66846d7 100644 --- a/packages/tools/src/bn/round.ts +++ b/packages/tools/src/math/round.ts @@ -1,7 +1,8 @@ -import { BN } from '@/bn/bn' -import { toDecimals } from '@/bn/decimals' import { BN_ROUNDING } from '@/enums' +import { BN } from './bn' +import { toDecimals } from './decimals' + export const round = (bn: BN, decimals: number, mode: BN_ROUNDING) => { const precisioned = toDecimals(bn.raw, BN.precision, decimals + 1).toString() diff --git a/packages/tools/src/duration.test.ts b/packages/tools/src/time/duration.test.ts similarity index 100% rename from packages/tools/src/duration.test.ts rename to packages/tools/src/time/duration.test.ts diff --git a/packages/tools/src/duration.ts b/packages/tools/src/time/duration.ts similarity index 100% rename from packages/tools/src/duration.ts rename to packages/tools/src/time/duration.ts diff --git a/packages/tools/src/time/index.ts b/packages/tools/src/time/index.ts new file mode 100644 index 00000000..ece339bc --- /dev/null +++ b/packages/tools/src/time/index.ts @@ -0,0 +1,2 @@ +export * from './duration' +export * from './time' diff --git a/packages/tools/src/time.test.ts b/packages/tools/src/time/time.test.ts similarity index 100% rename from packages/tools/src/time.test.ts rename to packages/tools/src/time/time.test.ts diff --git a/packages/tools/src/time.ts b/packages/tools/src/time/time.ts similarity index 100% rename from packages/tools/src/time.ts rename to packages/tools/src/time/time.ts diff --git a/packages/tools/src/types/bn.ts b/packages/tools/src/types/bn.ts index 9b584774..463c5802 100644 --- a/packages/tools/src/types/bn.ts +++ b/packages/tools/src/types/bn.ts @@ -1,5 +1,5 @@ -import { BN } from '@/bn' import { BN_ROUNDING } from '@/enums' +import { BN } from '@/math' export type BnConfigLike = number | BnConfig diff --git a/packages/tools/src/types/event-bus.ts b/packages/tools/src/types/event-bus.ts new file mode 100644 index 00000000..5e1835a7 --- /dev/null +++ b/packages/tools/src/types/event-bus.ts @@ -0,0 +1,22 @@ +import { EVENT_BUS_EVENTS } from '@/const' + +export type EventBusEventMap = + typeof EVENT_BUS_EVENTS & AdditionalEventBusMap + +export type EventBusEventName = string & + keyof EventBusEventMap + +export type EventBusEvent< + AdditionalEventBusMap extends object, + Payload = unknown, +> = { + name: EventBusEventName> + payload?: Payload +} + +export type EventBusEventHandler = (payload: Payload) => void + +export type EventBusEventEmitterEventMap = + { + [key in keyof EventBusEventMap]: unknown + } diff --git a/packages/tools/src/types/index.ts b/packages/tools/src/types/index.ts index 8e54b931..87e5b3f6 100644 --- a/packages/tools/src/types/index.ts +++ b/packages/tools/src/types/index.ts @@ -1,3 +1,5 @@ export * from './bn' +export * from './event-bus' export * from './event-emitter' +export * from './problem' export * from './time' diff --git a/packages/tools/src/types/problem.ts b/packages/tools/src/types/problem.ts new file mode 100644 index 00000000..7e8c9837 --- /dev/null +++ b/packages/tools/src/types/problem.ts @@ -0,0 +1,5 @@ +import type { EventBus } from '@/events' + +export type ProblemConfig = { + eventBus?: EventBus +} diff --git a/yarn.lock b/yarn.lock index ad746985..f6f023b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -478,6 +478,7 @@ __metadata: bignumber.js: ^9.1.1 dayjs: ^1.11.7 jest: ^29.5.0 + loglevel: ^1.8.1 tsc-alias: ^1.8.2 tslib: ^2.5.0 languageName: unknown @@ -6247,6 +6248,13 @@ __metadata: languageName: node linkType: hard +"loglevel@npm:^1.8.1": + version: 1.8.1 + resolution: "loglevel@npm:1.8.1" + checksum: a1a62db40291aaeaef2f612334c49e531bff71cc1d01a2acab689ab80d59e092f852ab164a5aedc1a752fdc46b7b162cb097d8a9eb2cf0b299511106c29af61d + languageName: node + linkType: hard + "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0"