diff --git a/docs/api/js-utils.asynctracker.filter.md b/docs/api/js-utils.asynctracker.filter.md new file mode 100644 index 0000000..27dd737 --- /dev/null +++ b/docs/api/js-utils.asynctracker.filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@apextoaster/js-utils](./js-utils.md) > [AsyncTracker](./js-utils.asynctracker.md) > [filter](./js-utils.asynctracker.filter.md) + +## AsyncTracker.filter property + +Signature: + +```typescript +filter: Optional; +``` diff --git a/docs/api/js-utils.asynctracker.getstack.md b/docs/api/js-utils.asynctracker.getstack.md index 844364b..fe53ee3 100644 --- a/docs/api/js-utils.asynctracker.getstack.md +++ b/docs/api/js-utils.asynctracker.getstack.md @@ -7,7 +7,7 @@ Signature: ```typescript -static getStack(): string; +getStack(): string; ``` Returns: diff --git a/docs/api/js-utils.asynctracker.md b/docs/api/js-utils.asynctracker.md index fa23d30..e27054e 100644 --- a/docs/api/js-utils.asynctracker.md +++ b/docs/api/js-utils.asynctracker.md @@ -24,6 +24,7 @@ export declare class AsyncTracker | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [filter](./js-utils.asynctracker.filter.md) | | Optional<StackFilter> | | | [size](./js-utils.asynctracker.size.md) | | number | | ## Methods @@ -34,5 +35,5 @@ export declare class AsyncTracker | [disable()](./js-utils.asynctracker.disable.md) | | | | [dump()](./js-utils.asynctracker.dump.md) | | | | [enable()](./js-utils.asynctracker.enable.md) | | | -| [getStack()](./js-utils.asynctracker.getstack.md) | static | | +| [getStack()](./js-utils.asynctracker.getstack.md) | | | diff --git a/docs/api/js-utils.isnil.md b/docs/api/js-utils.isnil.md index e8e4a14..e7f8e11 100644 --- a/docs/api/js-utils.isnil.md +++ b/docs/api/js-utils.isnil.md @@ -4,6 +4,8 @@ ## isNil() function +Check if a value is nil. + Signature: ```typescript diff --git a/docs/api/js-utils.md b/docs/api/js-utils.md index 422412f..fceeb7d 100644 --- a/docs/api/js-utils.md +++ b/docs/api/js-utils.md @@ -44,13 +44,13 @@ | [getOrDefault(map, key, defaultValue)](./js-utils.getordefault.md) | | | [getTestLogger(verbose)](./js-utils.gettestlogger.md) | | | [isDebug()](./js-utils.isdebug.md) | | -| [isNil(val)](./js-utils.isnil.md) | | +| [isNil(val)](./js-utils.isnil.md) | Check if a value is nil. | | [leftPad(val, min, fill)](./js-utils.leftpad.md) | | | [makeDict(map)](./js-utils.makedict.md) | Turns a map or dict into a dict | | [makeMap(val)](./js-utils.makemap.md) | Clone a map or map-like object into a new map. | | [mergeList(parts)](./js-utils.mergelist.md) | Merge arguments, which may or may not be arrays, into one return that is definitely an array. | | [mergeMap(target, source)](./js-utils.mergemap.md) | | -| [mustCoalesce(values)](./js-utils.mustcoalesce.md) | Return the first value that is not nil. | +| [mustCoalesce(values)](./js-utils.mustcoalesce.md) | Return the first value that is not nil.TODO: rename to mustDefault | | [mustExist(val)](./js-utils.mustexist.md) | Assert that a variable is not nil and return the value. | | [mustFind(list, predicate)](./js-utils.mustfind.md) | Find a value matching the given predicate or throw. | | [mustGet(map, key)](./js-utils.mustget.md) | Get an element from a Map and guard against nil values. | diff --git a/docs/api/js-utils.mustcoalesce.md b/docs/api/js-utils.mustcoalesce.md index ecc0a8a..9cf17f8 100644 --- a/docs/api/js-utils.mustcoalesce.md +++ b/docs/api/js-utils.mustcoalesce.md @@ -6,6 +6,8 @@ Return the first value that is not nil. +TODO: rename to mustDefault + Signature: ```typescript diff --git a/package.json b/package.json index c9f6532..e4b8d4a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/chai-as-promised": "7.1.2", "@types/lodash": "4.14.149", "@types/mocha": "7.0.2", + "@types/mock-fs": "^4.10.0", "@types/node": "13.9.5", "@types/sinon-chai": "3.2.3", "@types/source-map-support": "0.5.1", @@ -45,6 +46,7 @@ "eslint-plugin-sonarjs": "0.5.0", "lodash": "4.17.15", "mocha": "7.1.1", + "mock-fs": "^4.11.0", "noicejs": "3.0.1", "nyc": "15.0.0", "rollup": "2.3.1", diff --git a/src/AsyncTracker.ts b/src/AsyncTracker.ts index d2395b7..b8b7419 100644 --- a/src/AsyncTracker.ts +++ b/src/AsyncTracker.ts @@ -1,6 +1,6 @@ import { AsyncHook, createHook } from 'async_hooks'; -import { isNil } from './utils'; +import { isNil, Optional } from './utils'; import { isDebug } from './utils/Env'; export interface TrackedResource { @@ -9,6 +9,8 @@ export interface TrackedResource { type: string; } +export type StackFilter = (stack: string) => string; + /** * Async resource tracker using node's internal hooks. * @@ -16,15 +18,7 @@ export interface TrackedResource { * Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js */ export class AsyncTracker { - public static getStack(): string { - const err = new Error(); - if (isNil(err.stack)) { - return 'no stack trace available'; - } else { - return err.stack; // TODO: filterStack(err.stack); - } - } - + public filter: Optional; private readonly hook: AsyncHook; private readonly resources: Map; @@ -35,7 +29,7 @@ export class AsyncTracker { this.resources.delete(id); }, init: (id: number, type: string, triggerAsyncId: number) => { - const source = AsyncTracker.getStack(); + const source = this.getStack(); // @TODO: exclude async hooks, including this one this.resources.set(id, { source, @@ -49,6 +43,19 @@ export class AsyncTracker { }); } + public getStack(): string { + const err = new Error(); + if (isNil(err.stack)) { + return 'no stack trace available'; + } + + if (isNil(this.filter)) { + return err.stack; + } + + return this.filter(err.stack); + } + public clear() { this.resources.clear(); } diff --git a/src/utils/Async.ts b/src/utils/Async.ts index 8d5aedf..78100a2 100644 --- a/src/utils/Async.ts +++ b/src/utils/Async.ts @@ -1,4 +1,5 @@ import { TimeoutError } from '../error/TimeoutError'; +import { PredicateC0 } from '.'; /** * Resolve after a set amount of time. @@ -24,7 +25,7 @@ export function timeout(ms: number, oper: Promise): Promise { return Promise.race([limit, oper]); } -export async function waitFor(cb: () => boolean, step: number, count: number): Promise { +export async function waitFor(cb: PredicateC0, step: number, count: number): Promise { let accum = 0; while (accum < count) { await defer(step); diff --git a/src/utils/index.ts b/src/utils/index.ts index e3586e1..76937ed 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,26 +6,49 @@ import { NotFoundError } from '../error/NotFoundError'; /* eslint-disable-next-line @typescript-eslint/ban-types */ export type Nil = null | undefined; +export type SortAfter = 1; +export type SortBefore = -1; +export type SortEqual = 0; +export type SortOrder = SortAfter | SortBefore | SortEqual; + /** * Value that may be nil. */ export type Optional = T | Nil; /** - * Comparison (filter) predicate for a single value. + * Comparison predicate for arity 0 - assert? + */ +export type PredicateC0 = () => boolean; + +/** + * Comparison predicate for arity 1 - filter. */ export type PredicateC1 = (val: TVal, idx: number, list: Array) => boolean; /** - * Comparison (sort) predicate for two values. + * Comparison predicate for arity 2 - sort. */ -export type PredicateC2 = (pval: TVal, nval: TVal, idx: number, list: Array) => number; +export type PredicateC2 = (pval: TVal, nval: TVal, idx: number, list: Array) => SortOrder; /** - * Reduction predicate for two values. + * Transform predicate for arity 0 - constructor. + */ +export type PredicateR0 = () => TVal; + +/** + * Transform predicate for arity 1 - map. + */ +export type PredicateR1 = (val: TVal, idx: number, list: Array) => TVal; + +/** + * Transform predicate for arity 2 - reduce. */ export type PredicateR2 = (pval: TVal, nval: TVal, idx: number, list: Array) => TVal; +/** + * Check if a value is nil. + */ /* eslint-disable-next-line @typescript-eslint/ban-types */ export function isNil(val: Optional): val is Nil { /* eslint-disable-next-line no-null/no-null */ @@ -113,6 +136,8 @@ export function mustExist(val: Optional): T { /** * Return the first value that is not nil. + * + * TODO: rename to mustDefault */ export function mustCoalesce(...values: Array>): T { return mustFind(values, doesExist); diff --git a/test/utils/TestMap.ts b/test/utils/TestMap.ts index b7caa23..1bfa4ee 100644 --- a/test/utils/TestMap.ts +++ b/test/utils/TestMap.ts @@ -13,6 +13,7 @@ import { setOrPush, mergeMap, pushMergeMap, + normalizeMap, } from '../../src/utils/Map'; import { describeLeaks, itLeaks } from '../helpers/async'; @@ -206,4 +207,28 @@ describeLeaks('map utils', async () => { })).to.deep.equal(singleItem); }); }); + + describe('normalize map helper', () => { + it('should convert values into arrays of strings', () => { + const banVal = [Symbol()]; + const initial = new Map([ + ['bar', 'bin'], + ['ban', banVal], + ['toad', { + toString() { + return 'too'; + }, + }], + ]); + + const normalized = normalizeMap(initial); + + expect(normalized.bar).to.deep.equal(['bin']); + expect(normalized.ban).to.equal(banVal); + expect(normalized.toad).to.deep.equal(['too']); + }); + + /* ['foo', 1] */ + xit('should convert numbers into string values'); + }); }); diff --git a/test/utils/TestPidFile.ts b/test/utils/TestPidFile.ts new file mode 100644 index 0000000..b8b5043 --- /dev/null +++ b/test/utils/TestPidFile.ts @@ -0,0 +1,42 @@ +import { expect } from 'chai'; +import mockFS from 'mock-fs'; + +import { removePid, writePid } from '../../src'; +import { describeLeaks, itLeaks } from '../helpers/async'; + +const PID_PATH = 'foo'; +const PID_NAME = 'foo/test.pid'; + +describeLeaks('pid file utils', async () => { + beforeEach(() => { + mockFS({ + [PID_PATH]: mockFS.directory(), + }); + }); + + afterEach(() => { + mockFS.restore(); + }); + + itLeaks('should create a marker', async () => { + await writePid(PID_NAME); + + mockFS.restore(); + }); + + itLeaks('should not replace an existing marker', async () => { + await writePid(PID_NAME); + return expect(writePid(PID_PATH)).to.eventually.be.rejectedWith(Error); + }); + + itLeaks('should remove an existing marker', async () => { + await writePid(PID_NAME); + await removePid(PID_NAME); + + mockFS.restore(); + }); + + itLeaks('should fail to remove a missing marker', async () => + expect(removePid(PID_PATH)).to.eventually.be.rejectedWith(Error) + ); +}); diff --git a/test/utils/TestReflect.ts b/test/utils/TestReflect.ts index 7897df4..e4c1f1b 100644 --- a/test/utils/TestReflect.ts +++ b/test/utils/TestReflect.ts @@ -1,18 +1,33 @@ import { expect } from 'chai'; -import { getMethods } from '../../src/utils/Reflect'; +import { getMethods, getConstructor, constructorName } from '../../src/utils/Reflect'; + +class Test { + public foo() { /* noop */ } + public bar() { /* noop */ } +} describe('reflect utils', () => { describe('get methods helper', () => { it('should collect method functions', () => { - class Test { - public foo() { /* noop */ } - public bar() { /* noop */ } - } - const methods = getMethods(new Test()).values(); + /* eslint-disable @typescript-eslint/unbound-method */ expect(methods).to.include(Test.prototype.foo); expect(methods).to.include(Test.prototype.bar); }); }); + + describe('get constructor helper', () => { + it('should get the constructor from an instance', () => { + const instance = new Test(); + expect(getConstructor(instance)).to.equal(Test); + }); + }); + + describe('get constructor name helper', () => { + it('should get the constructor name from an instance', () => { + const instance = new Test(); + expect(constructorName(instance)).to.equal(Test.name); + }); + }); }); diff --git a/test/utils/TestSignal.ts b/test/utils/TestSignal.ts new file mode 100644 index 0000000..62f2f58 --- /dev/null +++ b/test/utils/TestSignal.ts @@ -0,0 +1,20 @@ +import { expect } from 'chai'; + +import { timeout } from '../../src/utils/Async'; +import { signal, SIGNAL_RESET } from '../../src/utils/Signal'; +import { describeLeaks, itLeaks } from '../helpers/async'; + +const MAX_SIGNAL_TIME = 500; + +describeLeaks('signal utils', async () => { + itLeaks('should wait for a signal', async () => { + const signalPromise = signal(SIGNAL_RESET); + + process.kill(process.pid, SIGNAL_RESET); + await timeout(MAX_SIGNAL_TIME, signalPromise); + + const signalValue = await signalPromise; + + expect(signalValue).to.equal(SIGNAL_RESET); + }); +}); diff --git a/test/utils/TestUtils.ts b/test/utils/TestUtils.ts index 5e13a03..80a0b1f 100644 --- a/test/utils/TestUtils.ts +++ b/test/utils/TestUtils.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { NotFoundError } from '../../src/error/NotFoundError'; -import { countOf, filterNil, mustFind } from '../../src/utils'; +import { countOf, filterNil, mustFind, defaultWhen, mustCoalesce } from '../../src/utils'; import { describeLeaks, itLeaks } from '../helpers/async'; describeLeaks('utils', async () => { @@ -42,4 +42,24 @@ describeLeaks('utils', async () => { }).to.throw(NotFoundError); }); }); + + describeLeaks('default when', async () => { + itLeaks('should return the first item when the condition is true', async () => { + expect(defaultWhen(true, 1, 2)).to.equal(1); + }); + + itLeaks('should return the second item otherwise', async () => { + expect(defaultWhen(false, 1, 2)).to.equal(2); + }); + }); + + describeLeaks('must coalesce helper', async () => { + /* eslint-disable no-null/no-null */ + itLeaks('should return the first existent value', async () => { + expect(mustCoalesce(null, null, 3, null)).to.equal(3); + expect(mustCoalesce(null, null, undefined, 'string')).to.equal('string'); + expect(mustCoalesce(null, undefined, [], null)).to.deep.equal([]); + }); + /* eslint-enable no-null/no-null */ + }); }); diff --git a/yarn.lock b/yarn.lock index ebef683..f4f7f79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -383,6 +383,13 @@ resolved "https://artifacts.apextoaster.com/repository/group-npm/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== +"@types/mock-fs@^4.10.0": + version "4.10.0" + resolved "https://artifacts.apextoaster.com/repository/group-npm/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b" + integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg== + dependencies: + "@types/node" "*" + "@types/node@*": version "12.7.8" resolved "https://artifacts.apextoaster.com/repository/group-npm/@types/node/-/node-12.7.8.tgz#cb1bf6800238898bc2ff6ffa5702c3cadd350708" @@ -2567,6 +2574,11 @@ mocha@7.1.1: yargs-parser "13.1.2" yargs-unparser "1.6.0" +mock-fs@^4.11.0: + version "4.11.0" + resolved "https://artifacts.apextoaster.com/repository/group-npm/mock-fs/-/mock-fs-4.11.0.tgz#0828107e4b843a6ba855ecebfe3c6e073b69db92" + integrity sha512-Yp4o3/ZA15wsXqJTT+R+9w2AYIkD1i80Lds47wDbuUhOvQvm+O2EfjFZSz0pMgZZSPHRhGxgcd2+GL4+jZMtdw== + modify-values@^1.0.0: version "1.0.1" resolved "https://artifacts.apextoaster.com/repository/group-npm/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"