From 84daaaf94a22c56eb1ed5174de09ccbc2e6bba23 Mon Sep 17 00:00:00 2001 From: Cole Lawrence Date: Sun, 8 May 2022 21:29:04 -0400 Subject: [PATCH] dev: Add configurable ITheatreLogger Addresses the lack of options we currently have for surfacing issues in our application via debugging tools. Prioritizes performance and usability (visually) over clarity in some places that could have been object mapped. A logger with three separate audiences: * `internal`: Logs for developers maintaining Theatre.js * `dev`: Logs for developers using Theatre.js * `public`: Logs for everyone This logger supports: * multiple logging levels (error, warn, debug, trace), * multiple audience levels (internal, dev, public), * multiple categories (general, todo, troubleshooting) * named and keyed loggers (e.g. `rootLogger.named("Project", project.id)`) * console styling with deterministic coloring * console devtool maintains accurate sourcemap link to logging origin (e.g. `coreExports.ts:71` as opposed to `logger.ts:45` or whatever) * swappable logger * customizable filtering * Accepts lazy `args`: `args: () => object` via `logger.lazy.("message", () => )` (e.g. `logger.lazy.debugDev("Loaded project state", () => ({ save: bigProject.exportToSaveable() }))`) --- packages/playground/src/shared/dom/index.tsx | 13 +- theatre/core/src/_coreLogger.ts | 43 + theatre/core/src/coreExports.ts | 12 +- theatre/core/src/projects/Project.ts | 20 +- theatre/core/src/projects/TheatreProject.ts | 20 + theatre/core/src/sequences/Sequence.ts | 13 +- theatre/core/src/sequences/TheatreSequence.ts | 8 +- .../interpolationTripleAtPosition.ts | 15 +- theatre/core/src/sheetObjects/SheetObject.ts | 11 +- theatre/core/src/sheets/Sheet.ts | 4 + .../src/_logger/logger.shouldLog.test.ts | 174 ++++ .../shared/src/_logger/logger.test-helpers.ts | 233 +++++ theatre/shared/src/_logger/logger.test.ts | 249 ++++++ theatre/shared/src/_logger/logger.ts | 796 ++++++++++++++++++ theatre/shared/src/logger.ts | 41 +- .../src/utils/numberRoundingUtils.test.ts | 115 +-- .../shared/src/utils/numberRoundingUtils.ts | 28 +- .../src/utils/redux/actionReducersBundle.ts | 6 +- 18 files changed, 1712 insertions(+), 89 deletions(-) create mode 100644 theatre/core/src/_coreLogger.ts create mode 100644 theatre/shared/src/_logger/logger.shouldLog.test.ts create mode 100644 theatre/shared/src/_logger/logger.test-helpers.ts create mode 100644 theatre/shared/src/_logger/logger.test.ts create mode 100644 theatre/shared/src/_logger/logger.ts diff --git a/packages/playground/src/shared/dom/index.tsx b/packages/playground/src/shared/dom/index.tsx index e461419..d1aa76f 100644 --- a/packages/playground/src/shared/dom/index.tsx +++ b/packages/playground/src/shared/dom/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom' import studio from '@theatre/studio' import {getProject} from '@theatre/core' import {Scene} from './Scene' +import {TheatreLoggerLevel} from '@theatre/shared/logger' /** * This is a basic example of using Theatre for manipulating the DOM. * @@ -13,6 +14,16 @@ import {Scene} from './Scene' studio.initialize() ReactDOM.render( - , + , document.getElementById('root'), ) diff --git a/theatre/core/src/_coreLogger.ts b/theatre/core/src/_coreLogger.ts new file mode 100644 index 0000000..4f32bd5 --- /dev/null +++ b/theatre/core/src/_coreLogger.ts @@ -0,0 +1,43 @@ +import type { + ITheatreLoggerConfig, + ITheatreLoggingConfig, +} from '@theatre/shared/logger' +import {TheatreLoggerLevel} from '@theatre/shared/logger' +import {createTheatreInternalLogger} from '@theatre/shared/logger' + +export type CoreLoggingConfig = Partial<{ + logger: ITheatreLoggerConfig + logging: ITheatreLoggingConfig +}> + +function noop() {} + +export function _coreLogger(config?: CoreLoggingConfig) { + const internalMin = config?.logging?.internal + ? config.logging.min ?? TheatreLoggerLevel.WARN + : Infinity // if not internal, then don't show any logs + const shouldDebugLogger = internalMin <= TheatreLoggerLevel.DEBUG + const shouldShowLoggerErrors = internalMin <= TheatreLoggerLevel.ERROR + const internal = createTheatreInternalLogger(undefined, { + _debug: shouldDebugLogger + ? console.debug.bind(console, '_coreLogger(TheatreInternalLogger) debug') + : noop, + _error: shouldShowLoggerErrors + ? console.error.bind(console, '_coreLogger(TheatreInternalLogger) error') + : noop, + }) + + if (config) { + const {logger, logging} = config + if (logger) internal.configureLogger(logger) + if (logging) internal.configureLogging(logging) + else { + // default to showing Theatre.js dev logs in non-production environments + internal.configureLogging({ + dev: process.env.NODE_ENV !== 'production', + }) + } + } + + return internal.getLogger().named('Theatre') +} diff --git a/theatre/core/src/coreExports.ts b/theatre/core/src/coreExports.ts index 8e2489b..c3bce09 100644 --- a/theatre/core/src/coreExports.ts +++ b/theatre/core/src/coreExports.ts @@ -17,6 +17,7 @@ import {isDerivation, valueDerivation} from '@theatre/dataverse' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import coreTicker from './coreTicker' import type {ProjectId} from '@theatre/shared/utils/ids' +import {_coreLogger} from './_coreLogger' export {types} /** @@ -45,7 +46,6 @@ export {types} * ``` */ export function getProject(id: string, config: IProjectConfig = {}): IProject { - const {...restOfConfig} = config const existingProject = projectsSingleton.get(id as ProjectId) if (existingProject) { if (process.env.NODE_ENV !== 'production') { @@ -61,20 +61,28 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject { return existingProject.publicApi } + const rootLogger = _coreLogger(config.experiments) + const plogger = rootLogger.named('Project', id) + if (process.env.NODE_ENV !== 'production') { validateName(id, 'projectName in Theatre.getProject(projectName)', true) validateProjectIdOrThrow(id) + plogger._debug('validated projectName', {projectName: id}) } if (config.state) { if (process.env.NODE_ENV !== 'production') { shallowValidateOnDiskState(id as ProjectId, config.state) + plogger._debug('shallow validated config.state on disk') } else { deepValidateOnDiskState(id as ProjectId, config.state) + plogger._debug('deep validated config.state on disk') } + } else { + plogger._debug('no config.state') } - return new TheatreProject(id, restOfConfig) + return new TheatreProject(id, config) } /** diff --git a/theatre/core/src/projects/Project.ts b/theatre/core/src/projects/Project.ts index 86d094d..9b8c975 100644 --- a/theatre/core/src/projects/Project.ts +++ b/theatre/core/src/projects/Project.ts @@ -18,9 +18,21 @@ import type { SheetId, SheetInstanceId, } from '@theatre/shared/utils/ids' +import type { + ILogger, + ITheatreLoggerConfig, + ITheatreLoggingConfig, +} from '@theatre/shared/logger' +import {_coreLogger} from '@theatre/core/_coreLogger' export type Conf = Partial<{ state: OnDiskState + experiments: ExperimentsConf +}> + +export type ExperimentsConf = Partial<{ + logger: ITheatreLoggerConfig + logging: ITheatreLoggingConfig }> export default class Project { @@ -47,14 +59,19 @@ export default class Project { private _studio: Studio | undefined type: 'Theatre_Project' = 'Theatre_Project' + readonly _logger: ILogger constructor( id: ProjectId, readonly config: Conf = {}, readonly publicApi: TheatreProject, ) { + this._logger = _coreLogger(config.experiments).named('Project', id) + this._logger.traceDev('creating project') this.address = {projectId: id} + // remove when logger is understood + this._logger._kapow('this is a "kapow"') const onDiskStateAtom = new Atom({ ahistoric: { ahistoricStuff: '', @@ -94,6 +111,7 @@ export default class Project { // let's give it one tick to attach itself if (!this._studio) { this._readyDeferred.resolve(undefined) + this._logger._trace('ready deferred resolved with no state') } }, 0) } else { @@ -118,7 +136,7 @@ export default class Project { `Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`, ) } else { - console.warn( + this._logger.warnDev( `Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`, ) return diff --git a/theatre/core/src/projects/TheatreProject.ts b/theatre/core/src/projects/TheatreProject.ts index 5809953..7b8ae4b 100644 --- a/theatre/core/src/projects/TheatreProject.ts +++ b/theatre/core/src/projects/TheatreProject.ts @@ -1,6 +1,10 @@ import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs' import Project from '@theatre/core/projects/Project' import type {ISheet} from '@theatre/core/sheets/TheatreSheet' +import type { + ITheatreLoggerConfig, + ITheatreLoggingConfig, +} from '@theatre/shared/logger' import type {ProjectAddress} from '@theatre/shared/utils/addresses' import type { ProjectId, @@ -19,6 +23,22 @@ export type IProjectConfig = { * The state of the project, as [exported](https://docs.theatrejs.com/in-depth/#exporting) by the studio. */ state?: $IntentionalAny + experiments?: IProjectConfigExperiments +} + +export type IProjectConfigExperiments = { + /** + * Defaults to using global `console` with style args. + * + * (TODO: check for browser environment before using style args) + */ + logger?: ITheatreLoggerConfig + /** + * Defaults: + * * `production` builds: console - error + * * `development` builds: console - error, warning + */ + logging?: ITheatreLoggingConfig } /** diff --git a/theatre/core/src/sequences/Sequence.ts b/theatre/core/src/sequences/Sequence.ts index 1725fb9..3dcc08d 100644 --- a/theatre/core/src/sequences/Sequence.ts +++ b/theatre/core/src/sequences/Sequence.ts @@ -14,7 +14,7 @@ import type { } from './playbackControllers/DefaultPlaybackController' import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController' import TheatreSequence from './TheatreSequence' -import logger from '@theatre/shared/logger' +import type {ILogger} from '@theatre/shared/logger' import type {ISequence} from '..' export type IPlaybackRange = [from: number, to: number] @@ -44,6 +44,7 @@ export default class Sequence { readonly pointer: ISequence['pointer'] = pointer({root: this, path: []}) readonly $$isIdentityDerivationProvider = true + readonly _logger: ILogger constructor( readonly _project: Project, @@ -52,6 +53,10 @@ export default class Sequence { readonly _subUnitsPerUnitD: IDerivation, playbackController?: IPlaybackController, ) { + this._logger = _project._logger + .named('Sheet', _sheet.address.sheetId) + .named('Instance', _sheet.address.sheetInstanceId) + this.address = {...this._sheet.address, sequenceName: 'default'} this.publicApi = new TheatreSequence(this) @@ -140,13 +145,13 @@ export default class Sequence { this.pause() if (process.env.NODE_ENV !== 'production') { if (typeof position !== 'number') { - logger.error( + this._logger.errorDev( `value t in sequence.position = t must be a number. ${typeof position} given`, ) position = 0 } if (position < 0) { - logger.error( + this._logger.errorDev( `sequence.position must be a positive number. ${position} given`, ) position = 0 @@ -226,7 +231,7 @@ export default class Sequence { } if (range[1] > sequenceDuration) { - logger.warn( + this._logger.warnDev( `Argument conf.range[1] in sequence.play(conf) cannot be longer than the duration of the sequence, which is ${sequenceDuration}s. ${JSON.stringify( range[1], )} given.`, diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index f0e052c..d94b4ea 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -1,4 +1,3 @@ -import logger from '@theatre/shared/logger' import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs' import {defer} from '@theatre/shared/utils/defer' import type Sequence from './Sequence' @@ -235,11 +234,12 @@ export default class TheatreSequence implements ISequence { direction: IPlaybackDirection }>, ): Promise { - if (privateAPI(this)._project.isReady()) { - return privateAPI(this).play(conf) + const priv = privateAPI(this) + if (priv._project.isReady()) { + return priv.play(conf) } else { if (process.env.NODE_ENV !== 'production') { - logger.warn( + priv._logger.warnDev( `You seem to have called sequence.play() before the project has finished loading.\n` + `This would **not** a problem in production when using '@theatre/core', since Theatre loads instantly in core mode. ` + `However, when using '@theatre/studio', it takes a few milliseconds for it to load your project's state, ` + diff --git a/theatre/core/src/sequences/interpolationTripleAtPosition.ts b/theatre/core/src/sequences/interpolationTripleAtPosition.ts index 55f9627..ad3fd89 100644 --- a/theatre/core/src/sequences/interpolationTripleAtPosition.ts +++ b/theatre/core/src/sequences/interpolationTripleAtPosition.ts @@ -5,7 +5,7 @@ import type { } from '@theatre/core/projects/store/types/SheetState_Historic' import type {IDerivation, Pointer} from '@theatre/dataverse' import {ConstantDerivation, prism, val} from '@theatre/dataverse' -import logger from '@theatre/shared/logger' +import type {IUtilContext} from '@theatre/shared/logger' import type {SerializableValue} from '@theatre/shared/utils/types' import UnitBezier from 'timing-function/lib/UnitBezier' @@ -26,6 +26,7 @@ export type InterpolationTriple = { // 2. Caching propConfig.deserializeAndSanitize(value) export default function interpolationTripleAtPosition( + ctx: IUtilContext, trackP: Pointer, timeD: IDerivation, ): IDerivation { @@ -37,9 +38,9 @@ export default function interpolationTripleAtPosition( if (!track) { return new ConstantDerivation(undefined) } else if (track.type === 'BasicKeyframedTrack') { - return _forKeyframedTrack(track, timeD) + return _forKeyframedTrack(ctx, track, timeD) } else { - logger.error(`Track type not yet supported.`) + ctx.logger.error(`Track type not yet supported.`) return new ConstantDerivation(undefined) } }, @@ -60,6 +61,7 @@ type IStartedState = { type IState = {started: false} | IStartedState function _forKeyframedTrack( + ctx: IUtilContext, track: BasicKeyframedTrack, timeD: IDerivation, ): IDerivation { @@ -70,7 +72,7 @@ function _forKeyframedTrack( const time = timeD.getValue() if (!state.started || time < state.validFrom || state.validTo <= time) { - stateRef.current = state = updateState(timeD, track) + stateRef.current = state = updateState(ctx, timeD, track) } return state.der.getValue() @@ -80,6 +82,7 @@ function _forKeyframedTrack( const undefinedConstD = new ConstantDerivation(undefined) function updateState( + ctx: IUtilContext, progressionD: IDerivation, track: BasicKeyframedTrack, ): IStartedState { @@ -100,7 +103,7 @@ function updateState( if (!currentKeyframe) { if (process.env.NODE_ENV !== 'production') { - logger.error(`Bug here`) + ctx.logger.error(`Bug here`) } return states.error } @@ -112,7 +115,7 @@ function updateState( return states.beforeFirstKeyframe(currentKeyframe) } else { if (process.env.NODE_ENV !== 'production') { - logger.error(`Bug here`) + ctx.logger.error(`Bug here`) } return states.error // note: uncomment these if we support starting with currentPointIndex != 0 diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 319de7a..8ab798a 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -25,6 +25,7 @@ import type SheetObjectTemplate from './SheetObjectTemplate' import TheatreSheetObject from './TheatreSheetObject' import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes' import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' +import type {ILogger, IUtilContext} from '@theatre/shared/logger' /** * Internally, the sheet's actual configured value is not a specific type, since we @@ -50,12 +51,20 @@ export default class SheetObject implements IdentityDerivationProvider { readonly publicApi: TheatreSheetObject private readonly _initialValue = new Atom({}) private readonly _cache = new SimpleCache() + readonly _logger: ILogger + private readonly _internalUtilCtx: IUtilContext constructor( readonly sheet: Sheet, readonly template: SheetObjectTemplate, readonly nativeObject: unknown, ) { + this._logger = sheet._logger.named( + 'SheetObject', + template.address.objectKey, + ) + this._logger._trace('creating object') + this._internalUtilCtx = {logger: this._logger.downgrade.internal()} this.address = { ...template.address, sheetInstanceId: sheet.address.sheetInstanceId, @@ -229,7 +238,7 @@ export default class SheetObject implements IdentityDerivationProvider { const timeD = this.sheet.getSequence().positionDerivation - return interpolationTripleAtPosition(trackP, timeD) + return interpolationTripleAtPosition(this._internalUtilCtx, trackP, timeD) } get propsP(): Pointer { diff --git a/theatre/core/src/sheets/Sheet.ts b/theatre/core/src/sheets/Sheet.ts index 54158d5..dc06b39 100644 --- a/theatre/core/src/sheets/Sheet.ts +++ b/theatre/core/src/sheets/Sheet.ts @@ -8,6 +8,7 @@ import {Atom, valueDerivation} from '@theatre/dataverse' import type SheetTemplate from './SheetTemplate' import type {ObjectAddressKey, SheetInstanceId} from '@theatre/shared/utils/ids' import type {StrictRecord} from '@theatre/shared/utils/types' +import type {ILogger} from '@theatre/shared/logger' type SheetObjectMap = StrictRecord @@ -28,11 +29,14 @@ export default class Sheet { readonly project: Project readonly objectsP = this._objects.pointer type: 'Theatre_Sheet' = 'Theatre_Sheet' + readonly _logger: ILogger constructor( readonly template: SheetTemplate, public readonly instanceId: SheetInstanceId, ) { + this._logger = template.project._logger.named('Sheet', instanceId) + this._logger._trace('creating sheet') this.project = template.project this.address = { ...template.address, diff --git a/theatre/shared/src/_logger/logger.shouldLog.test.ts b/theatre/shared/src/_logger/logger.shouldLog.test.ts new file mode 100644 index 0000000..8073843 --- /dev/null +++ b/theatre/shared/src/_logger/logger.shouldLog.test.ts @@ -0,0 +1,174 @@ +import {TheatreLoggerLevel, _LoggerLevel} from './logger' +import {_loggerShouldLog} from './logger' + +describe('Theatre internal logger: shouldLog', () => { + testIncludes( + 'TRACE dev/internal', + { + dev: true, + internal: true, + min: TheatreLoggerLevel.TRACE, + }, + { + _ERROR: true, + _HMM: true, + _TODO: true, + ERROR_DEV: true, + ERROR_PUBLIC: true, + _KAPOW: true, + _WARN: true, + WARN_DEV: true, + WARN_PUBLIC: true, + _DEBUG: true, + DEBUG_DEV: true, + _TRACE: true, + TRACE_DEV: true, + }, + ) + + testIncludes( + 'DEBUG dev/internal', + { + dev: true, + internal: true, + min: TheatreLoggerLevel.DEBUG, + }, + { + _ERROR: true, + _HMM: true, + _TODO: true, + ERROR_DEV: true, + ERROR_PUBLIC: true, + _WARN: true, + _KAPOW: true, + WARN_DEV: true, + WARN_PUBLIC: true, + _DEBUG: true, + DEBUG_DEV: true, + _TRACE: false, + TRACE_DEV: false, + }, + ) + + testIncludes( + 'TRACE dev', + { + dev: true, + internal: false, + min: TheatreLoggerLevel.TRACE, + }, + { + _ERROR: false, + _HMM: false, + _KAPOW: false, + _TODO: false, + ERROR_DEV: true, + ERROR_PUBLIC: true, + _WARN: false, + WARN_DEV: true, + WARN_PUBLIC: true, + _DEBUG: false, + DEBUG_DEV: true, + _TRACE: false, + TRACE_DEV: true, + }, + ) + + testIncludes( + 'TRACE', + { + dev: false, + internal: false, + min: TheatreLoggerLevel.TRACE, + }, + { + _ERROR: false, + _HMM: false, + _KAPOW: false, + _TODO: false, + ERROR_DEV: false, + ERROR_PUBLIC: true, + _WARN: false, + WARN_DEV: false, + WARN_PUBLIC: true, + _DEBUG: false, + DEBUG_DEV: false, + _TRACE: false, + TRACE_DEV: false, + }, + ) + + testIncludes( + 'WARN dev', + { + dev: true, + internal: false, + min: TheatreLoggerLevel.WARN, + }, + { + _ERROR: false, + _HMM: false, + _KAPOW: false, + _TODO: false, + ERROR_DEV: true, + ERROR_PUBLIC: true, + _WARN: false, + WARN_DEV: true, + WARN_PUBLIC: true, + _DEBUG: false, + DEBUG_DEV: false, + _TRACE: false, + TRACE_DEV: false, + }, + ) + + testIncludes( + 'TRACE internal', + { + dev: false, + internal: true, + min: TheatreLoggerLevel.TRACE, + }, + { + _ERROR: true, + _HMM: true, + _TODO: true, + ERROR_DEV: false, + ERROR_PUBLIC: true, + _KAPOW: true, + _WARN: true, + WARN_DEV: false, + WARN_PUBLIC: true, + _DEBUG: true, + DEBUG_DEV: false, + _TRACE: true, + TRACE_DEV: false, + }, + ) +}) + +function testIncludes( + name: string, + config: { + dev: boolean + internal: boolean + min: TheatreLoggerLevel + }, + expectations: {[P in keyof typeof _LoggerLevel]: boolean}, +) { + test.each(Object.entries(expectations))( + `${name} + %s = %s`, + (level, expectIsIncluded) => { + const actual = _loggerShouldLog(config, _LoggerLevel[level]) + if (actual !== expectIsIncluded) { + const stackless = new Error( + `Expected shouldLog({ dev: ${config.dev}, internal: ${ + config.internal + }, max: ${TheatreLoggerLevel[config.min]} }, ${level}) = ${actual}`, + ) + stackless.stack = undefined // stack is not useful in test + throw stackless + } + }, + ) +} diff --git a/theatre/shared/src/_logger/logger.test-helpers.ts b/theatre/shared/src/_logger/logger.test-helpers.ts new file mode 100644 index 0000000..b14e8cd --- /dev/null +++ b/theatre/shared/src/_logger/logger.test-helpers.ts @@ -0,0 +1,233 @@ +import type { + ITheatreConsoleLogger, + _LogFns, + ITheatreInternalLoggerOptions, + IUtilLogger, + ILogger, +} from './logger' +import {createTheatreInternalLogger} from './logger' + +const DEBUG_LOGGER = false + +function noop() {} + +export function describeLogger( + name: string, + body: (setup: () => ReturnType) => void, +) { + describe(name, () => { + body( + setupFn.bind(null, { + _debug: DEBUG_LOGGER ? console.log.bind(console, name) : noop, + _error: console.error.bind(console, name), + }), + ) + }) +} + +function setupFn(options: ITheatreInternalLoggerOptions) { + const con = spyConsole() + const internal = createTheatreInternalLogger(con, options) + function t(logger = internal.getLogger()) { + return { + expectExcluded(kind: keyof _LogFns) { + try { + const message = `${kind} message` + logger[kind](message) + expect(con.debug).not.toBeCalled() + expect(con.info).not.toBeCalled() + expect(con.warn).not.toBeCalled() + expect(con.error).not.toBeCalled() + } catch (err) { + throw new LoggerTestError( + `Expected logger.${kind}(...) excluded from logging\n${( + err as Error + ).toString()}`, + ) + } + }, + expectIncluded( + kind: keyof _LogFns, + expectOutputted: keyof ITheatreConsoleLogger, + includes: TestLoggerIncludes = [], + ) { + try { + const message = `${kind} message` + logger[kind](message) + if (expectOutputted !== 'debug') { + expect(con.debug).not.toBeCalled() + } + if (expectOutputted !== 'info') { + expect(con.info).not.toBeCalled() + } + if (expectOutputted !== 'warn') { + expect(con.warn).not.toBeCalled() + } + if (expectOutputted !== 'error') { + expect(con.error).not.toBeCalled() + } + expectLastCalledWith(con[expectOutputted], includes) + } catch (err) { + throw new LoggerTestError( + `Expected logger.${kind}(...) included and outputted via console.${expectOutputted}(...)\n${( + err as Error + ).toString()}`, + ) + } + con[expectOutputted].mockReset() + }, + named(name: string, key?: string | number) { + return t(logger.named(name, key)) + }, + downgrade: objMap( + logger.downgrade, + ([audience, downgradeFn]) => + () => + setupUtilLogger(downgradeFn(), audience, con), + ), + } + } + return { + internal, + con, + t, + } +} + +function expectLastCalledWith( + fn: jest.MockInstance, + includes: TestLoggerIncludes, +) { + expect(fn).toBeCalled() + if (includes.length > 0) { + const [lastCall] = fn.mock.calls + const concat = lastCall.filter(Boolean).map(String).join(', ') + const errors = includes.flatMap((includeTest) => { + if (typeof includeTest === 'string') { + return concat.includes(includeTest) + ? [] + : [`didn't include ${JSON.stringify(includeTest)}`] + } else if ('test' in includeTest) { + return includeTest.test(concat) + ? [] + : [`didn't match ${String(includeTest)}`] + } else if (typeof includeTest.not === 'string') { + return concat.includes(includeTest.not) + ? [`wasn't supposed to include ${JSON.stringify(includeTest.not)}`] + : [] + } else if ('test' in includeTest.not) { + return includeTest.not.test(concat) + ? [`wasn't supposed to match ${String(includeTest.not)}`] + : [] + } + }) + if (errors.length > 0) { + throw new LoggerTestError( + `Last called with ${JSON.stringify(concat)}, but ${errors.join(', ')}`, + ) + } + } +} + +function objMap( + template: T, + eachEntry:

(entry: [name: P, value: T[P]]) => U, +): {[P in keyof T]: U} { + // @ts-ignore + return Object.fromEntries( + Object.entries(template).map((entry) => { + // @ts-ignore + return [entry[0], eachEntry(entry)] + }), + ) +} + +type TestLoggerIncludes = ((string | RegExp) | {not: string | RegExp})[] + +function setupUtilLogger( + logger: IUtilLogger, + audience: keyof ILogger['downgrade'], + con: jest.Mocked, +) { + return { + named(name: string, key?: string) { + return setupUtilLogger(logger.named(name, key), audience, con) + }, + expectExcluded(kind: keyof IUtilLogger) { + try { + const message = `${audience} ${kind} message` + logger[kind](message) + expect(con.debug).not.toBeCalled() + expect(con.info).not.toBeCalled() + expect(con.warn).not.toBeCalled() + expect(con.error).not.toBeCalled() + } catch (err) { + throw new LoggerTestError( + `Expected "${audience}" logger.${kind}(...) excluded from logging\n${( + err as Error + ).toString()}`, + ) + } + }, + expectIncluded( + kind: keyof IUtilLogger, + expectOutputted: keyof ITheatreConsoleLogger, + includes: TestLoggerIncludes = [], + ) { + try { + const message = `${audience} ${kind} message` + logger[kind](message) + if (expectOutputted !== 'debug') { + expect(con.debug).not.toBeCalled() + } + if (expectOutputted !== 'info') { + expect(con.info).not.toBeCalled() + } + if (expectOutputted !== 'warn') { + expect(con.warn).not.toBeCalled() + } + if (expectOutputted !== 'error') { + expect(con.error).not.toBeCalled() + } + expectLastCalledWith(con[expectOutputted], includes) + } catch (err) { + throw new LoggerTestError( + `Expected "${audience}" logger.${kind}(...) included and outputted via console.${expectOutputted}(...)\n${( + err as Error + ).toString()}`, + ) + } + con[expectOutputted].mockReset() + }, + } +} + +function spyConsole(): jest.Mocked { + return { + debug: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + } +} + +// remove these lines from thrown errors +const AT_NODE_INTERNAL_RE = /^\s*at.+node:internal.+/gm +const AT_TEST_HELPERS_RE = /^\s*(at|[^@]+@).+test\-helpers.+/gm + +/** `TestError` removes the invariant line & test-helpers from the `Error.stack` */ +class LoggerTestError extends Error { + found: any + constructor(message: string, found?: any) { + super(message) + if (found) { + this.found = found + } + // const before = this.stack + // prettier-ignore + this.stack = this.stack + ?.replace(AT_TEST_HELPERS_RE, "") + .replace(AT_NODE_INTERNAL_RE, "") + // console.error({ before, after: this.stack }) + } +} diff --git a/theatre/shared/src/_logger/logger.test.ts b/theatre/shared/src/_logger/logger.test.ts new file mode 100644 index 0000000..ef98d86 --- /dev/null +++ b/theatre/shared/src/_logger/logger.test.ts @@ -0,0 +1,249 @@ +import {TheatreLoggerLevel} from './logger' +import {describeLogger} from './logger.test-helpers' + +describeLogger('Theatre internal logger', (setup) => { + describe('default logger', () => { + test('it reports public messages', () => { + const t = setup().t() + + t.expectIncluded('errorPublic', 'error') + t.expectIncluded('warnPublic', 'warn') + }) + + test('it does not report dev messages', () => { + const t = setup().t() + + t.expectExcluded('errorDev') + t.expectExcluded('warnDev') + t.expectExcluded('debugDev') + t.expectExcluded('traceDev') + }) + + test('it does not report internal messages', () => { + const t = setup().t() + + t.expectExcluded('_hmm') + t.expectExcluded('_kapow') + t.expectExcluded('_debug') + t.expectExcluded('_trace') + }) + }) + + describe('custom logging', () => { + test('it can include all dev and internal logs', () => { + const h = setup() + + const initial = h.t() + + h.internal.configureLogging({ + dev: true, + internal: true, + min: TheatreLoggerLevel.TRACE, + }) + + const t = h.t() + + // initial logger will not have been able to acknowledge + // the logging config update. + initial.expectExcluded('_hmm') + initial.expectExcluded('errorDev') + + t.expectIncluded('_hmm', 'error') + t.expectIncluded('_kapow', 'warn') + t.expectIncluded('_debug', 'info') + t.expectIncluded('_trace', 'debug') + + t.expectIncluded('errorDev', 'error') + t.expectIncluded('warnDev', 'warn') + t.expectIncluded('debugDev', 'info') + t.expectIncluded('traceDev', 'debug') + }) + + test('it can include WARN level dev and internal logs', () => { + const h = setup() + + h.internal.configureLogging({ + dev: true, + internal: true, + min: TheatreLoggerLevel.WARN, + }) + + const t = h.t() + + t.expectIncluded('_hmm', 'error') + t.expectIncluded('_kapow', 'warn') + t.expectIncluded('errorDev', 'error') + t.expectIncluded('warnDev', 'warn') + + t.expectExcluded('_debug') + t.expectExcluded('_trace') + t.expectExcluded('debugDev') + t.expectExcluded('traceDev') + }) + + test('it can include WARN level dev logs', () => { + const h = setup() + + h.internal.configureLogging({ + dev: true, + min: TheatreLoggerLevel.WARN, + }) + + const t = h.t() + + t.expectIncluded('errorDev', 'error') + t.expectIncluded('warnDev', 'warn') + + t.expectExcluded('_hmm') + t.expectExcluded('_kapow') + t.expectExcluded('_debug') + t.expectExcluded('_trace') + t.expectExcluded('debugDev') + t.expectExcluded('traceDev') + }) + }) + + describe('named and keys', () => { + test('default with no name has no colors', () => { + const h = setup() + const app = h.t().named('App') + const appK2 = app.named('K', 1) + + app.expectIncluded('errorPublic', 'error', ['App', {not: /K.*#1/}]) + app.expectIncluded('warnPublic', 'warn', ['App', {not: /K.*#1/}]) + appK2.expectIncluded('errorPublic', 'error', ['App', /App.*K.*#1/]) + appK2.expectIncluded('warnPublic', 'warn', ['App', /App.*K.*#1/]) + }) + }) + + describe('named gets colors', () => { + test('default with no name has no colors', () => { + const h = setup() + const t = h.t() + + t.expectIncluded('errorPublic', 'error', [{not: 'color:'}]) + t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}]) + }) + + test('default with name has color', () => { + const h = setup() + const t = h.t().named('Test1') + + t.expectIncluded('errorPublic', 'error', ['color:', 'Test1']) + t.expectIncluded('warnPublic', 'warn', ['color:', 'Test1']) + }) + + test('consoleStyle: false with name does not have color', () => { + const h = setup() + h.internal.configureLogging({ + consoleStyle: false, + }) + + const t = h.t().named('Test1') + + t.expectIncluded('errorPublic', 'error', [{not: 'color:'}, 'Test1']) + t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1']) + }) + + test('console style: false with name does not have color', () => { + const h = setup() + h.internal.configureLogger({ + type: 'console', + console: h.con, + style: false, + }) + + const t = h.t().named('Test1') + + t.expectIncluded('errorPublic', 'error', [{not: 'color:'}, 'Test1']) + t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1']) + }) + }) + describe('downgrade', () => { + test('.downgrade.public() with defaults', () => { + const h = setup() + + const publ = h.t().downgrade.public() + + publ.expectIncluded('error', 'error') + publ.expectIncluded('warn', 'warn') + publ.expectExcluded('debug') + publ.expectExcluded('trace') + }) + + test('.downgrade.dev() with defaults', () => { + const h = setup() + + const dev = h.t().downgrade.dev() + + dev.expectExcluded('error') + dev.expectExcluded('warn') + dev.expectExcluded('debug') + dev.expectExcluded('trace') + }) + + test('.downgrade.internal() with defaults', () => { + const h = setup() + + const internal = h.t().downgrade.internal() + + internal.expectExcluded('error') + internal.expectExcluded('warn') + internal.expectExcluded('debug') + internal.expectExcluded('trace') + }) + + test('.downgrade.internal() can be named', () => { + const h = setup() + + h.internal.configureLogging({ + internal: true, + min: TheatreLoggerLevel.TRACE, + }) + + const internal = h.t().downgrade.internal() + const appleInternal = internal.named('Apple') + + internal.expectIncluded('error', 'error', [{not: 'Apple'}]) + internal.expectIncluded('warn', 'warn', [{not: 'Apple'}]) + internal.expectIncluded('debug', 'info', [{not: 'Apple'}]) + internal.expectIncluded('trace', 'debug', [{not: 'Apple'}]) + + appleInternal.expectIncluded('error', 'error', ['Apple']) + appleInternal.expectIncluded('warn', 'warn', ['Apple']) + appleInternal.expectIncluded('debug', 'info', ['Apple']) + appleInternal.expectIncluded('trace', 'debug', ['Apple']) + }) + + test('.downgrade.public() debug/trace warns internal', () => { + const h = setup() + { + h.internal.configureLogging({ + internal: true, + }) + const publ = h.t().downgrade.public() + + publ.expectIncluded('error', 'error', [{not: 'filtered out'}]) + publ.expectIncluded('warn', 'warn', [{not: 'filtered out'}]) + + // warnings go through internal loggers since public loggers do not have a trace or debug level + publ.expectIncluded('debug', 'warn', ['filtered out']) + publ.expectIncluded('trace', 'warn', ['filtered out']) + } + + { + h.internal.configureLogging({ + dev: true, + }) + const publ = h.t().downgrade.public() + + publ.expectIncluded('error', 'error', [{not: /filtered out/}]) + publ.expectIncluded('warn', 'warn', [{not: /filtered out/}]) + + // warnings only go through internal loggers + publ.expectExcluded('debug') + publ.expectExcluded('trace') + } + }) + }) +}) diff --git a/theatre/shared/src/_logger/logger.ts b/theatre/shared/src/_logger/logger.ts new file mode 100644 index 0000000..ac70b19 --- /dev/null +++ b/theatre/shared/src/_logger/logger.ts @@ -0,0 +1,796 @@ +/** @public configuration type */ +export interface ITheatreLogger { + error(level: ITheatreLogMeta, message: string, args?: Loggable): void + warn(level: ITheatreLogMeta, message: string, args?: Loggable): void + debug(level: ITheatreLogMeta, message: string, args?: Loggable): void + trace(level: ITheatreLogMeta, message: string, args?: Loggable): void +} + +type ITheatreLogMeta = Readonly<{ + audience: 'public' | 'dev' | 'internal' + category: 'general' | 'todo' | 'troubleshooting' + level: TheatreLoggerLevel +}> + +/** @public configuration type */ +export interface ITheatreConsoleLogger { + /** ERROR level logs */ + error(message: string, ...args: any[]): void + /** WARN level logs */ + warn(message: string, ...args: any[]): void + /** DEBUG level logs */ + info(message: string, ...args: any[]): void + /** TRACE level logs */ + debug(message: string, ...args: any[]): void +} + +/** + * "Downgraded" {@link ILogger} for passing down to utility functions. + * + * A util logger is usually back by some specific {@link _Audience}. + */ +export interface IUtilLogger { + /** Usually equivalent to `console.error`. */ + error(message: string, args?: object): void + /** Usually equivalent to `console.warn`. */ + warn(message: string, args?: object): void + /** Usually equivalent to `console.info`. */ + debug(message: string, args?: object): void + /** Usually equivalent to `console.debug`. */ + trace(message: string, args?: object): void + named(name: string, key?: string): IUtilLogger +} + +type Loggable = Record +type LogFn = (message: string, args?: Loggable) => void +/** + * Allow for the arguments to only be computed if the level is included. + * If the level is not included, then the fn will still be passed to the filtered + * function. + */ +type LazyLogFn = (message: string, args: () => Loggable) => void + +function lazy(f: LogFn): LazyLogFn { + return function lazyLogIncluded(m, lazyArg) { + return f(m, lazyArg()) + } +} + +export type _LogFns = Readonly< + { + [P in keyof typeof LEVELS]: LogFn + } +> + +export type _LazyLogFns = Readonly< + { + [P in keyof typeof LEVELS]: LazyLogFn + } +> + +/** Internal library logger */ +export interface ILogger extends _LogFns { + named(name: string, key?: string | number): ILogger + lazy: _LazyLogFns + readonly downgrade: { + internal(): IUtilLogger + dev(): IUtilLogger + public(): IUtilLogger + } +} + +export type ITheatreLoggerConfig = + | /** default {@link console} */ + 'console' + | { + type: 'console' + /** default `true` */ + style?: boolean + /** default {@link console} */ + console?: ITheatreConsoleLogger + } + | { + type: 'named' + named(names: string[]): ITheatreLogger + } + | { + type: 'keyed' + keyed( + nameAndKeys: { + name: string + key?: string | number + }[], + ): ITheatreLogger + } + +export type ITheatreLogSource = {names: {name: string; key?: number | string}[]} + +export type ITheatreLogIncludes = { + /** + * General information max level. + * e.g. `Project imported might be corrupted` + */ + min?: TheatreLoggerLevel + /** + * Include logs meant for developers using Theatre.js + * e.g. `Created new project 'Abc' with options {...}` + * + * defaults to `true` if `internal: true` or defaults to `false`. + */ + dev?: boolean + /** + * Include logs meant for internal development of Theatre.js + * e.g. `Migrated project 'Abc' { duration_ms: 34, from_version: 1, to_version: 3, imported_settings: false }` + * + * defaults to `false` + */ + internal?: boolean +} + +export type ITheatreLoggingConfig = ITheatreLogIncludes & { + include?: (source: ITheatreLogSource) => ITheatreLogIncludes + consoleStyle?: boolean +} + +/** @internal */ +enum _Category { + GENERAL = 1 << 0, + TODO = 1 << 1, + TROUBLESHOOTING = 1 << 2, +} + +/** @internal */ +enum _Audience { + /** Logs for developers of Theatre.js */ + INTERNAL = 1 << 3, + /** Logs for developers using Theatre.js */ + DEV = 1 << 4, + /** Logs for users of the app using Theatre.js */ + PUBLIC = 1 << 5, +} + +export enum TheatreLoggerLevel { + TRACE = 1 << 6, + DEBUG = 1 << 7, + WARN = 1 << 8, + ERROR = 1 << 9, +} + +/** + * @internal Theatre internal "dev" levels are odd numbers + * + * You can check if a level is odd quickly by doing `level & 1 === 1` + */ +export enum _LoggerLevel { + /** The highest logging level number. */ + ERROR_PUBLIC = TheatreLoggerLevel.ERROR | + _Audience.PUBLIC | + _Category.GENERAL, + ERROR_DEV = TheatreLoggerLevel.ERROR | _Audience.DEV | _Category.GENERAL, + /** @internal this was an unexpected event */ + _HMM = TheatreLoggerLevel.ERROR | + _Audience.INTERNAL | + _Category.TROUBLESHOOTING, + _TODO = TheatreLoggerLevel.ERROR | _Audience.INTERNAL | _Category.TODO, + _ERROR = TheatreLoggerLevel.ERROR | _Audience.INTERNAL | _Category.GENERAL, + WARN_PUBLIC = TheatreLoggerLevel.WARN | _Audience.PUBLIC | _Category.GENERAL, + WARN_DEV = TheatreLoggerLevel.WARN | _Audience.DEV | _Category.GENERAL, + /** @internal surface this in this moment, but it probably shouldn't be left in the code after debugging. */ + _KAPOW = TheatreLoggerLevel.WARN | + _Audience.INTERNAL | + _Category.TROUBLESHOOTING, + _WARN = TheatreLoggerLevel.WARN | _Audience.INTERNAL | _Category.GENERAL, + DEBUG_DEV = TheatreLoggerLevel.DEBUG | _Audience.DEV | _Category.GENERAL, + /** @internal debug logs for implementation details */ + _DEBUG = TheatreLoggerLevel.DEBUG | _Audience.INTERNAL | _Category.GENERAL, + /** trace logs like when the project is saved */ + TRACE_DEV = TheatreLoggerLevel.TRACE | _Audience.DEV | _Category.GENERAL, + /** + * The lowest logging level number. + * @internal trace logs for implementation details + */ + _TRACE = TheatreLoggerLevel.TRACE | _Audience.INTERNAL | _Category.GENERAL, +} + +const LEVELS = { + _hmm: getLogMeta(_LoggerLevel._HMM), + _todo: getLogMeta(_LoggerLevel._TODO), + _error: getLogMeta(_LoggerLevel._ERROR), + errorDev: getLogMeta(_LoggerLevel.ERROR_DEV), + errorPublic: getLogMeta(_LoggerLevel.ERROR_PUBLIC), + _kapow: getLogMeta(_LoggerLevel._KAPOW), + _warn: getLogMeta(_LoggerLevel._WARN), + warnDev: getLogMeta(_LoggerLevel.WARN_DEV), + warnPublic: getLogMeta(_LoggerLevel.WARN_PUBLIC), + _debug: getLogMeta(_LoggerLevel._DEBUG), + debugDev: getLogMeta(_LoggerLevel.DEBUG_DEV), + _trace: getLogMeta(_LoggerLevel._TRACE), + traceDev: getLogMeta(_LoggerLevel.TRACE_DEV), +} + +function getLogMeta(level: _LoggerLevel): ITheatreLogMeta { + return Object.freeze({ + audience: hasFlag(level, _Audience.INTERNAL) + ? 'internal' + : hasFlag(level, _Audience.DEV) + ? 'dev' + : 'public', + category: hasFlag(level, _Category.TROUBLESHOOTING) + ? 'troubleshooting' + : hasFlag(level, _Category.TODO) + ? 'todo' + : 'general', + level: + // I think this is equivalent... but I'm not using it until we have tests. + // this code won't really impact performance much anyway, since it's just computed once + // up front. + // level & + // (TheatreLoggerLevel.TRACE | + // TheatreLoggerLevel.DEBUG | + // TheatreLoggerLevel.WARN | + // TheatreLoggerLevel.ERROR), + hasFlag(level, TheatreLoggerLevel.ERROR) + ? TheatreLoggerLevel.ERROR + : hasFlag(level, TheatreLoggerLevel.WARN) + ? TheatreLoggerLevel.WARN + : hasFlag(level, TheatreLoggerLevel.DEBUG) + ? TheatreLoggerLevel.DEBUG + : // no other option + TheatreLoggerLevel.TRACE, + }) +} + +/** + * This is a helper function to determine whether the logger level has a bit flag set. + * + * Flags are interesting, because they give us an opportunity to very easily set up filtering + * based on category and level. This is not available from public api, yet, but it's a good + * start. + */ +function hasFlag(level: _LoggerLevel, flag: number): boolean { + return (level & flag) === flag +} + +/** + * @internal + * + * You'd think max, means number "max", but since we use this system of bit flags, + * we actually need to go the other way, with comparisons being math less than. + * + * NOTE: Keep this in the same file as {@link _Audience} to ensure basic compilers + * can inline the enum values. + */ +function shouldLog( + includes: Required, + level: _LoggerLevel, +) { + return ( + ((level & _Audience.PUBLIC) === _Audience.PUBLIC + ? true + : (level & _Audience.DEV) === _Audience.DEV + ? includes.dev + : (level & _Audience.INTERNAL) === _Audience.INTERNAL + ? includes.internal + : false) && includes.min <= level + ) +} + +export {shouldLog as _loggerShouldLog} + +type InternalLoggerStyleRef = { + italic?: RegExp + bold?: RegExp + color?: (name: string) => string + collapseOnRE: RegExp + cssMemo: Map + css(this: InternalLoggerStyleRef, name: string): string + collapsed(this: InternalLoggerStyleRef, name: string): string +} + +type InternalLoggerRef = { + loggingConsoleStyle: boolean + loggerConsoleStyle: boolean + includes: Required + filtered: ( + this: ITheatreLogSource, + level: _LoggerLevel, + message: string, + args?: Loggable | (() => Loggable), + ) => void + include: (obj: ITheatreLogSource) => ITheatreLogIncludes + create: (obj: ITheatreLogSource) => ILogger + creatExt: (obj: ITheatreLogSource) => ITheatreLogger + style: InternalLoggerStyleRef + named( + this: InternalLoggerRef, + parent: ITheatreLogSource, + name: string, + key?: number | string, + ): ILogger +} + +const DEFAULTS: InternalLoggerRef = { + loggingConsoleStyle: true, + loggerConsoleStyle: true, + includes: Object.freeze({ + internal: false, + dev: false, + min: TheatreLoggerLevel.WARN, + }), + filtered: function defaultFiltered() {}, + include: function defaultInclude() { + return {} + }, + create: null!, + creatExt: null!, + named(this: InternalLoggerRef, parent, name, key) { + return this.create({ + names: [...parent.names, {name, key}], + }) + }, + style: { + bold: undefined, // /Service$/ + italic: undefined, // /Model$/ + cssMemo: new Map([ + // handle empty names so we don't have to check for + // name.length > 0 during this.css('') + ['', ''], + // bring a specific override + // ["Marker", "color:#aea9ff;font-size:0.75em;text-transform:uppercase"] + ]), + collapseOnRE: /[a-z- ]+/g, + color: undefined, + // create collapsed name + // insert collapsed name into cssMemo with original's style + collapsed(this, name) { + if (name.length < 5) return name + const collapsed = name.replace(this.collapseOnRE, '') + if (!this.cssMemo.has(collapsed)) { + this.cssMemo.set(collapsed, this.css(name)) + } + return collapsed + }, + css(this, name): string { + const found = this.cssMemo.get(name) + if (found) return found + let css = `color:${ + this.color?.(name) ?? + `hsl(${ + (name.charCodeAt(0) + name.charCodeAt(name.length - 1)) % 360 + }, 100%, 60%)` + }` + if (this.bold?.test(name)) { + css += ';font-weight:600' + } + if (this.italic?.test(name)) { + css += ';font-style:italic' + } + this.cssMemo.set(name, css) + return css + }, + }, +} + +/** @internal */ +export type ITheatreInternalLogger = { + configureLogger(config: ITheatreLoggerConfig): void + configureLogging(config: ITheatreLoggingConfig): void + getLogger(): ILogger +} + +export type ITheatreInternalLoggerOptions = { + _error?: (message: string, args?: object) => void + _debug?: (message: string, args?: object) => void +} + +export function createTheatreInternalLogger( + useConsole: ITheatreConsoleLogger = console, + // Not yet, used, but good pattern to have in case we want to log something + // or report something interesting. + _options: ITheatreInternalLoggerOptions = {}, +): ITheatreInternalLogger { + const ref: InternalLoggerRef = {...DEFAULTS, includes: {...DEFAULTS.includes}} + const createConsole = { + styled: createConsoleLoggerStyled.bind(ref, useConsole), + noStyle: createConsoleLoggerNoStyle.bind(ref, useConsole), + } + const createExtBound = createExtLogger.bind(ref) + function getConCreate() { + return ref.loggingConsoleStyle && ref.loggerConsoleStyle + ? createConsole.styled + : createConsole.noStyle + } + ref.create = getConCreate() + + return { + configureLogger(config) { + if (config === 'console') { + ref.loggerConsoleStyle = DEFAULTS.loggerConsoleStyle + ref.create = getConCreate() + } else if (config.type === 'console') { + ref.loggerConsoleStyle = config.style ?? DEFAULTS.loggerConsoleStyle + ref.create = getConCreate() + } else if (config.type === 'keyed') { + ref.creatExt = (source) => config.keyed(source.names) + ref.create = createExtBound + } else if (config.type === 'named') { + ref.creatExt = configNamedToKeyed.bind(null, config.named) + ref.create = createExtBound + } + }, + configureLogging(config) { + ref.includes.dev = config.dev ?? DEFAULTS.includes.dev + ref.includes.internal = config.internal ?? DEFAULTS.includes.internal + ref.includes.min = config.min ?? DEFAULTS.includes.min + ref.include = config.include ?? DEFAULTS.include + ref.loggingConsoleStyle = + config.consoleStyle ?? DEFAULTS.loggingConsoleStyle + ref.create = getConCreate() + }, + getLogger() { + return ref.create({names: []}) + }, + } +} + +/** used by `configureLogger` for `'named'` */ +function configNamedToKeyed( + namedFn: (names: string[]) => ITheatreLogger, + source: ITheatreLogSource, +): ITheatreLogger { + const names: string[] = [] + for (let {name, key} of source.names) { + names.push(key == null ? name : `${name} (${key})`) + } + return namedFn(names) +} + +function createExtLogger( + this: InternalLoggerRef, + source: ITheatreLogSource, +): ILogger { + const includes = {...this.includes, ...this.include(source)} + const f = this.filtered + const named = this.named.bind(this, source) + const ext = this.creatExt(source) + + const _HMM = shouldLog(includes, _LoggerLevel._HMM) + const _TODO = shouldLog(includes, _LoggerLevel._TODO) + const _ERROR = shouldLog(includes, _LoggerLevel._ERROR) + const ERROR_DEV = shouldLog(includes, _LoggerLevel.ERROR_DEV) + const ERROR_PUBLIC = shouldLog(includes, _LoggerLevel.ERROR_PUBLIC) + const _WARN = shouldLog(includes, _LoggerLevel._WARN) + const _KAPOW = shouldLog(includes, _LoggerLevel._KAPOW) + const WARN_DEV = shouldLog(includes, _LoggerLevel.WARN_DEV) + const WARN_PUBLIC = shouldLog(includes, _LoggerLevel.WARN_PUBLIC) + const _DEBUG = shouldLog(includes, _LoggerLevel._DEBUG) + const DEBUG_DEV = shouldLog(includes, _LoggerLevel.DEBUG_DEV) + const _TRACE = shouldLog(includes, _LoggerLevel._TRACE) + const TRACE_DEV = shouldLog(includes, _LoggerLevel.TRACE_DEV) + const _hmm = _HMM + ? ext.error.bind(ext, LEVELS._hmm) + : f.bind(source, _LoggerLevel._HMM) + const _todo = _TODO + ? ext.error.bind(ext, LEVELS._todo) + : f.bind(source, _LoggerLevel._TODO) + const _error = _ERROR + ? ext.error.bind(ext, LEVELS._error) + : f.bind(source, _LoggerLevel._ERROR) + const errorDev = ERROR_DEV + ? ext.error.bind(ext, LEVELS.errorDev) + : f.bind(source, _LoggerLevel.ERROR_DEV) + const errorPublic = ERROR_PUBLIC + ? ext.error.bind(ext, LEVELS.errorPublic) + : f.bind(source, _LoggerLevel.ERROR_PUBLIC) + const _kapow = _KAPOW + ? ext.warn.bind(ext, LEVELS._kapow) + : f.bind(source, _LoggerLevel._KAPOW) + const _warn = _WARN + ? ext.warn.bind(ext, LEVELS._warn) + : f.bind(source, _LoggerLevel._WARN) + const warnDev = WARN_DEV + ? ext.warn.bind(ext, LEVELS.warnDev) + : f.bind(source, _LoggerLevel.WARN_DEV) + const warnPublic = WARN_PUBLIC + ? ext.warn.bind(ext, LEVELS.warnPublic) + : f.bind(source, _LoggerLevel.WARN_DEV) + const _debug = _DEBUG + ? ext.debug.bind(ext, LEVELS._debug) + : f.bind(source, _LoggerLevel._DEBUG) + const debugDev = DEBUG_DEV + ? ext.debug.bind(ext, LEVELS.debugDev) + : f.bind(source, _LoggerLevel.DEBUG_DEV) + const _trace = _TRACE + ? ext.trace.bind(ext, LEVELS._trace) + : f.bind(source, _LoggerLevel._TRACE) + const traceDev = TRACE_DEV + ? ext.trace.bind(ext, LEVELS.traceDev) + : f.bind(source, _LoggerLevel.TRACE_DEV) + const logger: ILogger = { + _hmm, + _todo, + _error, + errorDev, + errorPublic, + _kapow, + _warn, + warnDev, + warnPublic, + _debug, + debugDev, + _trace, + traceDev, + lazy: { + _hmm: _HMM ? lazy(_hmm) : _hmm, + _todo: _TODO ? lazy(_todo) : _todo, + _error: _ERROR ? lazy(_error) : _error, + errorDev: ERROR_DEV ? lazy(errorDev) : errorDev, + errorPublic: ERROR_PUBLIC ? lazy(errorPublic) : errorPublic, + _kapow: _KAPOW ? lazy(_kapow) : _kapow, + _warn: _WARN ? lazy(_warn) : _warn, + warnDev: WARN_DEV ? lazy(warnDev) : warnDev, + warnPublic: WARN_PUBLIC ? lazy(warnPublic) : warnPublic, + _debug: _DEBUG ? lazy(_debug) : _debug, + debugDev: DEBUG_DEV ? lazy(debugDev) : debugDev, + _trace: _TRACE ? lazy(_trace) : _trace, + traceDev: TRACE_DEV ? lazy(traceDev) : traceDev, + }, + // + named, + downgrade: { + internal() { + return { + debug: logger._debug, + error: logger._error, + warn: logger._warn, + trace: logger._trace, + named(name, key) { + return logger.named(name, key).downgrade.internal() + }, + } + }, + dev() { + return { + debug: logger.debugDev, + error: logger.errorDev, + warn: logger.warnDev, + trace: logger.traceDev, + named(name, key) { + return logger.named(name, key).downgrade.dev() + }, + } + }, + public() { + return { + error: logger.errorPublic, + warn: logger.warnPublic, + debug(message, obj) { + logger._warn(`(public "debug" filtered out) ${message}`, obj) + }, + trace(message, obj) { + logger._warn(`(public "trace" filtered out) ${message}`, obj) + }, + named(name, key) { + return logger.named(name, key).downgrade.public() + }, + } + }, + }, + } + + return logger +} + +function createConsoleLoggerStyled( + this: InternalLoggerRef, + con: ITheatreConsoleLogger, + source: ITheatreLogSource, +): ILogger { + const includes = {...this.includes, ...this.include(source)} + + const styleArgs: any[] = [] + let prefix = '' + for (let i = 0; i < source.names.length; i++) { + const {name, key} = source.names[i] + prefix += ` %c${name}` + styleArgs.push(this.style.css(name)) + if (key != null) { + const keyStr = `%c#${key}` + prefix += keyStr + styleArgs.push(this.style.css(keyStr)) + } + } + + const f = this.filtered + const named = this.named.bind(this, source) + const prefixArr = [prefix, ...styleArgs] + return _createConsoleLogger( + f, + source, + includes, + con, + prefixArr, + styledKapowPrefix(prefixArr), + named, + ) +} + +function styledKapowPrefix(args: ReadonlyArray): ReadonlyArray { + const start = args.slice(0) + for (let i = 1; i < start.length; i++) + // add big font to all part styles + start[i] += ';background-color:#e0005a;padding:2px;color:white' + return start +} + +function createConsoleLoggerNoStyle( + this: InternalLoggerRef, + con: ITheatreConsoleLogger, + source: ITheatreLogSource, +): ILogger { + const includes = {...this.includes, ...this.include(source)} + + let prefix = '' + for (let i = 0; i < source.names.length; i++) { + const {name, key} = source.names[i] + prefix += ` ${name}` + if (key != null) { + prefix += `#${key}` + } + } + + const f = this.filtered + const named = this.named.bind(this, source) + const prefixArr = [prefix] + return _createConsoleLogger( + f, + source, + includes, + con, + prefixArr, + prefixArr, + named, + ) +} + +/** Used by {@link createConsoleLoggerNoStyle} and {@link createConsoleLoggerStyled} */ +function _createConsoleLogger( + f: ( + this: ITheatreLogSource, + level: _LoggerLevel, + message: string, + args?: object | undefined, + ) => void, + source: ITheatreLogSource, + includes: {min: TheatreLoggerLevel; dev: boolean; internal: boolean}, + con: ITheatreConsoleLogger, + prefix: ReadonlyArray, + kapowPrefix: ReadonlyArray, + named: (name: string, key?: string | number | undefined) => ILogger, +) { + const _HMM = shouldLog(includes, _LoggerLevel._HMM) + const _TODO = shouldLog(includes, _LoggerLevel._TODO) + const _ERROR = shouldLog(includes, _LoggerLevel._ERROR) + const ERROR_DEV = shouldLog(includes, _LoggerLevel.ERROR_DEV) + const ERROR_PUBLIC = shouldLog(includes, _LoggerLevel.ERROR_PUBLIC) + const _WARN = shouldLog(includes, _LoggerLevel._WARN) + const _KAPOW = shouldLog(includes, _LoggerLevel._KAPOW) + const WARN_DEV = shouldLog(includes, _LoggerLevel.WARN_DEV) + const WARN_PUBLIC = shouldLog(includes, _LoggerLevel.WARN_PUBLIC) + const _DEBUG = shouldLog(includes, _LoggerLevel._DEBUG) + const DEBUG_DEV = shouldLog(includes, _LoggerLevel.DEBUG_DEV) + const _TRACE = shouldLog(includes, _LoggerLevel._TRACE) + const TRACE_DEV = shouldLog(includes, _LoggerLevel.TRACE_DEV) + const _hmm = _HMM + ? con.error.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._HMM) + const _todo = _TODO + ? con.error.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._TODO) + const _error = _ERROR + ? con.error.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._ERROR) + const errorDev = ERROR_DEV + ? con.error.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.ERROR_DEV) + const errorPublic = ERROR_PUBLIC + ? con.error.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.ERROR_PUBLIC) + const _kapow = _KAPOW + ? con.warn.bind(con, ...kapowPrefix) + : f.bind(source, _LoggerLevel._KAPOW) + const _warn = _WARN + ? con.warn.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._WARN) + const warnDev = WARN_DEV + ? con.warn.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.WARN_DEV) + const warnPublic = WARN_PUBLIC + ? con.warn.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.WARN_DEV) + const _debug = _DEBUG + ? con.info.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._DEBUG) + const debugDev = DEBUG_DEV + ? con.info.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.DEBUG_DEV) + const _trace = _TRACE + ? con.debug.bind(con, ...prefix) + : f.bind(source, _LoggerLevel._TRACE) + const traceDev = TRACE_DEV + ? con.debug.bind(con, ...prefix) + : f.bind(source, _LoggerLevel.TRACE_DEV) + const logger: ILogger = { + _hmm, + _todo, + _error, + errorDev, + errorPublic, + _kapow, + _warn, + warnDev, + warnPublic, + _debug, + debugDev, + _trace, + traceDev, + lazy: { + _hmm: _HMM ? lazy(_hmm) : _hmm, + _todo: _TODO ? lazy(_todo) : _todo, + _error: _ERROR ? lazy(_error) : _error, + errorDev: ERROR_DEV ? lazy(errorDev) : errorDev, + errorPublic: ERROR_PUBLIC ? lazy(errorPublic) : errorPublic, + _kapow: _KAPOW ? lazy(_kapow) : _kapow, + _warn: _WARN ? lazy(_warn) : _warn, + warnDev: WARN_DEV ? lazy(warnDev) : warnDev, + warnPublic: WARN_PUBLIC ? lazy(warnPublic) : warnPublic, + _debug: _DEBUG ? lazy(_debug) : _debug, + debugDev: DEBUG_DEV ? lazy(debugDev) : debugDev, + _trace: _TRACE ? lazy(_trace) : _trace, + traceDev: TRACE_DEV ? lazy(traceDev) : traceDev, + }, + // + named, + downgrade: { + internal() { + return { + debug: logger._debug, + error: logger._error, + warn: logger._warn, + trace: logger._trace, + named(name, key) { + return logger.named(name, key).downgrade.internal() + }, + } + }, + dev() { + return { + debug: logger.debugDev, + error: logger.errorDev, + warn: logger.warnDev, + trace: logger.traceDev, + named(name, key) { + return logger.named(name, key).downgrade.dev() + }, + } + }, + public() { + return { + error: logger.errorPublic, + warn: logger.warnPublic, + debug(message, obj) { + logger._warn(`(public "debug" filtered out) ${message}`, obj) + }, + trace(message, obj) { + logger._warn(`(public "trace" filtered out) ${message}`, obj) + }, + named(name, key) { + return logger.named(name, key).downgrade.public() + }, + } + }, + }, + } + + return logger +} diff --git a/theatre/shared/src/logger.ts b/theatre/shared/src/logger.ts index 651a833..74eb7c2 100644 --- a/theatre/shared/src/logger.ts +++ b/theatre/shared/src/logger.ts @@ -1,8 +1,37 @@ -const logger = { - log: console.log, - warn: console.warn, - error: console.error, - trace: console.trace, +export type { + ILogger, + IUtilLogger, + ITheatreConsoleLogger, + ITheatreLogIncludes, + ITheatreLogSource, + ITheatreLoggerConfig, + ITheatreLoggingConfig, + ITheatreInternalLogger, +} from './_logger/logger' +import {createTheatreInternalLogger, TheatreLoggerLevel} from './_logger/logger' +import type {IUtilLogger} from './_logger/logger' +export {TheatreLoggerLevel, createTheatreInternalLogger} from './_logger/logger' + +/** + * Common object interface for the context to pass in to utility functions. + * + * Prefer to pass this into utility function rather than an {@link IUtilLogger}. + */ +export interface IUtilContext { + readonly logger: IUtilLogger } -export default logger +const internal = createTheatreInternalLogger(console, { + _debug: function () {}, + _error: function () {}, +}) + +internal.configureLogging({ + dev: true, + min: TheatreLoggerLevel.TRACE, +}) + +export default internal + .getLogger() + .named('Theatre.js (default logger)') + .downgrade.dev() diff --git a/theatre/shared/src/utils/numberRoundingUtils.test.ts b/theatre/shared/src/utils/numberRoundingUtils.test.ts index 316dac5..2ca41a2 100644 --- a/theatre/shared/src/utils/numberRoundingUtils.test.ts +++ b/theatre/shared/src/utils/numberRoundingUtils.test.ts @@ -1,3 +1,4 @@ +import type {IUtilContext} from '@theatre/shared/logger' import { getLastMultipleOf, numberOfDecimals, @@ -27,36 +28,44 @@ const example = ( ) } +const CTX: IUtilContext = { + get logger(): never { + throw new Error('unexpected logger access in test example') + }, +} + describe(`numberRoundingUtils()`, () => { describe(`roundestNumberBetween()`, () => { - example(roundestNumberBetween, [0.1, 1.1], 1) - example(roundestNumberBetween, [0.1111111123, 0.2943439448], 0.25) - example(roundestNumberBetween, [0.19, 0.23], 0.2) - example(roundestNumberBetween, [-0.19, 0.23], 0) - example(roundestNumberBetween, [-0.19, -0.02], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, -0.022], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, -0.022234324], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, 0.0222222], 0) - example(roundestNumberBetween, [-0.19, 0.02], 0) + example(roundestNumberBetween, [CTX, 0.1, 1.1], 1) + example(roundestNumberBetween, [CTX, 0.1111111123, 0.2943439448], 0.25) + example(roundestNumberBetween, [CTX, 0.19, 0.23], 0.2) + example(roundestNumberBetween, [CTX, -0.19, 0.23], 0) + example(roundestNumberBetween, [CTX, -0.19, -0.02], -0.1, {debug: false}) + example(roundestNumberBetween, [CTX, -0.19, -0.022], -0.1, {debug: false}) + example(roundestNumberBetween, [CTX, -0.19, -0.022234324], -0.1, { + debug: false, + }) + example(roundestNumberBetween, [CTX, -0.19, 0.0222222], 0) + example(roundestNumberBetween, [CTX, -0.19, 0.02], 0) example( roundestNumberBetween, - [22304.2398427391, 22304.2398427393], + [CTX, 22304.2398427391, 22304.2398427393], 22304.2398427392, ) - example(roundestNumberBetween, [22304.2398427391, 22304.4], 22304.25) - example(roundestNumberBetween, [902, 901], 902) - example(roundestNumberBetween, [-10, -5], -10) - example(roundestNumberBetween, [-5, -10], -10) - example(roundestNumberBetween, [-10, -5], -10) + example(roundestNumberBetween, [CTX, 22304.2398427391, 22304.4], 22304.25) + example(roundestNumberBetween, [CTX, 902, 901], 902) + example(roundestNumberBetween, [CTX, -10, -5], -10) + example(roundestNumberBetween, [CTX, -5, -10], -10) + example(roundestNumberBetween, [CTX, -10, -5], -10) example( roundestNumberBetween, - [-0.00876370109231405, -2.909374013346118e-50], + [CTX, -0.00876370109231405, -2.909374013346118e-50], 0, {debug: false}, ) example( roundestNumberBetween, - [0.059449443526800295, 0.06682093143783596], + [CTX, 0.059449443526800295, 0.06682093143783596], 0.06, {debug: false}, ) @@ -73,7 +82,7 @@ describe(`numberRoundingUtils()`, () => { const from = toPrecision(getRandomNumber()) const to = toPrecision(getRandomNumber()) - const result = roundestNumberBetween(from, to) + const result = roundestNumberBetween(CTX, from, to) if (from < to) { if (result < from || result > to) { throw new Error(`Invalid: ${from} ${to} ${result}`) @@ -87,43 +96,43 @@ describe(`numberRoundingUtils()`, () => { }) }) describe(`roundestIntegerBetween`, () => { - example(roundestIntegerBetween, [-1, 6], 0, {}) - example(roundestIntegerBetween, [0, 6], 0, {}) - example(roundestIntegerBetween, [-1, 0], 0, {}) - example(roundestIntegerBetween, [-1850, -1740], -1750, {}) - example(roundestIntegerBetween, [1, 6], 5, {}) - example(roundestIntegerBetween, [1, 5], 5) - example(roundestIntegerBetween, [1, 2], 2) - example(roundestIntegerBetween, [1, 10], 10) - example(roundestIntegerBetween, [1, 12], 10) - example(roundestIntegerBetween, [11, 15], 15) - example(roundestIntegerBetween, [101, 102], 102, {debug: true}) - example(roundestIntegerBetween, [11, 14, false], 12) - example(roundestIntegerBetween, [11, 14, true], 12.5) - example(roundestIntegerBetween, [11, 12], 12) - example(roundestIntegerBetween, [11, 12], 12, {}) - example(roundestIntegerBetween, [10, 90], 50) - example(roundestIntegerBetween, [10, 100], 100) - example(roundestIntegerBetween, [10, 110], 100) - example(roundestIntegerBetween, [9, 100], 10) - example(roundestIntegerBetween, [9, 1100], 10) - example(roundestIntegerBetween, [9, 699], 10) - example(roundestIntegerBetween, [9, 400], 10) - example(roundestIntegerBetween, [9, 199], 10) - example(roundestIntegerBetween, [9, 1199], 10) - example(roundestIntegerBetween, [1921, 1998], 1950) - example(roundestIntegerBetween, [1921, 2020], 2000) - example(roundestIntegerBetween, [1601, 1998], 1750) - example(roundestIntegerBetween, [1919, 1921], 1920) - example(roundestIntegerBetween, [1919, 1919], 1919) - example(roundestIntegerBetween, [3901, 3902], 3902) - example(roundestIntegerBetween, [901, 902], 902) + example(roundestIntegerBetween, [CTX, -1, 6], 0, {}) + example(roundestIntegerBetween, [CTX, 0, 6], 0, {}) + example(roundestIntegerBetween, [CTX, -1, 0], 0, {}) + example(roundestIntegerBetween, [CTX, -1850, -1740], -1750, {}) + example(roundestIntegerBetween, [CTX, 1, 6], 5, {}) + example(roundestIntegerBetween, [CTX, 1, 5], 5) + example(roundestIntegerBetween, [CTX, 1, 2], 2) + example(roundestIntegerBetween, [CTX, 1, 10], 10) + example(roundestIntegerBetween, [CTX, 1, 12], 10) + example(roundestIntegerBetween, [CTX, 11, 15], 15) + example(roundestIntegerBetween, [CTX, 101, 102], 102, {debug: true}) + example(roundestIntegerBetween, [CTX, 11, 14, false], 12) + example(roundestIntegerBetween, [CTX, 11, 14, true], 12.5) + example(roundestIntegerBetween, [CTX, 11, 12], 12) + example(roundestIntegerBetween, [CTX, 11, 12], 12, {}) + example(roundestIntegerBetween, [CTX, 10, 90], 50) + example(roundestIntegerBetween, [CTX, 10, 100], 100) + example(roundestIntegerBetween, [CTX, 10, 110], 100) + example(roundestIntegerBetween, [CTX, 9, 100], 10) + example(roundestIntegerBetween, [CTX, 9, 1100], 10) + example(roundestIntegerBetween, [CTX, 9, 699], 10) + example(roundestIntegerBetween, [CTX, 9, 400], 10) + example(roundestIntegerBetween, [CTX, 9, 199], 10) + example(roundestIntegerBetween, [CTX, 9, 1199], 10) + example(roundestIntegerBetween, [CTX, 1921, 1998], 1950) + example(roundestIntegerBetween, [CTX, 1921, 2020], 2000) + example(roundestIntegerBetween, [CTX, 1601, 1998], 1750) + example(roundestIntegerBetween, [CTX, 1919, 1921], 1920) + example(roundestIntegerBetween, [CTX, 1919, 1919], 1919) + example(roundestIntegerBetween, [CTX, 3901, 3902], 3902) + example(roundestIntegerBetween, [CTX, 901, 902], 902) }) describe(`roundestFloat()`, () => { - example(roundestFloat, [0.19, 0.2122], 0.2) - example(roundestFloat, [0.19, 0.31], 0.25) - example(roundestFloat, [0.19, 0.41], 0.25) - example(roundestFloat, [0.19, 1.9], 0.5) + example(roundestFloat, [CTX, 0.19, 0.2122], 0.2) + example(roundestFloat, [CTX, 0.19, 0.31], 0.25) + example(roundestFloat, [CTX, 0.19, 0.41], 0.25) + example(roundestFloat, [CTX, 0.19, 1.9], 0.5) }) describe(`numberOfDecimals()`, () => { example(numberOfDecimals, [1.1], 1) diff --git a/theatre/shared/src/utils/numberRoundingUtils.ts b/theatre/shared/src/utils/numberRoundingUtils.ts index b72b2ca..2a0784d 100644 --- a/theatre/shared/src/utils/numberRoundingUtils.ts +++ b/theatre/shared/src/utils/numberRoundingUtils.ts @@ -1,25 +1,29 @@ import padEnd from 'lodash-es/padEnd' -import logger from '@theatre/shared/logger' +import type {IUtilContext} from '@theatre/shared/logger' -export function roundestNumberBetween(_a: number, _b: number): number { +export function roundestNumberBetween( + ctx: IUtilContext, + _a: number, + _b: number, +): number { if (_b < _a) { - return roundestNumberBetween(_b, _a) + return roundestNumberBetween(ctx, _b, _a) } if (_a < 0 && _b < 0) { - return noMinusZero(roundestNumberBetween(-_b, -_a) * -1) + return noMinusZero(roundestNumberBetween(ctx, -_b, -_a) * -1) } if (_a <= 0 && _b >= 0) return 0 const aCeiling = Math.ceil(_a) if (aCeiling <= _b) { - return roundestIntegerBetween(aCeiling, Math.floor(_b)) + return roundestIntegerBetween(ctx, aCeiling, Math.floor(_b)) } else { const [a, b] = [_a, _b] const integer = Math.floor(a) - return integer + roundestFloat(a - integer, b - integer) + return integer + roundestFloat(ctx, a - integer, b - integer) } } @@ -28,6 +32,7 @@ const halvesAndQuartiles = [5, 2.5, 7.5] const multipliersWithoutQuartiles = [5, 2, 4, 6, 8, 1, 3, 7, 9] export function roundestIntegerBetween( + ctx: IUtilContext, _a: number, _b: number, decimalsAllowed: boolean = true, @@ -77,7 +82,9 @@ export function roundestIntegerBetween( base = highestTotalFound if (currentExponentiationOfTen === 1) { - logger.error(`Coudn't find a human-readable number between ${a} and ${b}`) + ctx.logger.error( + `Coudn't find a human-readable number between ${a} and ${b}`, + ) return _a } else { currentExponentiationOfTen /= 10 @@ -126,7 +133,11 @@ export const stringifyNumber = (n: number): string => { /** * it is expected that both args are 0 \< arg \< 1 */ -export const roundestFloat = (a: number, b: number): number => { +export const roundestFloat = ( + ctx: IUtilContext, + a: number, + b: number, +): number => { const inString = { a: stringifyNumber(a), b: stringifyNumber(b), @@ -160,6 +171,7 @@ export const roundestFloat = (a: number, b: number): number => { } const roundestInt = roundestIntegerBetween( + ctx, parseInt(withPaddedDecimals.a, 10) * Math.pow(10, maxNumberOfLeadingZeros), parseInt(withPaddedDecimals.b, 10) * Math.pow(10, maxNumberOfLeadingZeros), true, diff --git a/theatre/studio/src/utils/redux/actionReducersBundle.ts b/theatre/studio/src/utils/redux/actionReducersBundle.ts index bae0e2c..585206d 100644 --- a/theatre/studio/src/utils/redux/actionReducersBundle.ts +++ b/theatre/studio/src/utils/redux/actionReducersBundle.ts @@ -1,4 +1,4 @@ -import logger from '@theatre/shared/logger' +import type {IUtilContext} from '@theatre/shared/logger' import type {$IntentionalAny, GenericAction} from '@theatre/shared/utils/types' import mapValues from 'lodash-es/mapValues' @@ -13,7 +13,7 @@ export type PayloadTypeOfReducer< > = Parameters[1]['payload'] const actionReducersBundle = - () => + (ctx: IUtilContext) => < Reducers extends Record< string, @@ -37,7 +37,7 @@ const actionReducersBundle = const {type} = action const innerReducer = (reducers as $IntentionalAny)[type] if (!innerReducer) { - logger.error(`Unkown action type '${type}'`) + ctx.logger.error(`Unkown action type '${type}'`) return prevState } const newState: State = innerReducer(prevState, action)