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"