@beecode/msh-util 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/array-util.d.ts +2 -2
- package/lib/array-util.js +2 -2
- package/package.json +134 -127
- package/src/array-util.test.ts +50 -0
- package/src/array-util.ts +26 -0
- package/src/class-factory-pattern.test.ts +39 -0
- package/src/class-factory-pattern.ts +29 -0
- package/src/express/error-handler.test.ts +44 -0
- package/src/express/error-handler.ts +25 -0
- package/src/index.ts +25 -0
- package/src/joi-util.test.ts +192 -0
- package/src/joi-util.ts +65 -0
- package/src/memoize-factory.test.ts +40 -0
- package/src/memoize-factory.ts +27 -0
- package/src/object-util.test.ts +360 -0
- package/src/object-util.ts +127 -0
- package/src/regex-util.test.ts +25 -0
- package/src/regex-util.ts +11 -0
- package/src/single-threshold-promise.test.ts +91 -0
- package/src/single-threshold-promise.ts +56 -0
- package/src/singleton/async.test.ts +122 -0
- package/src/singleton/async.ts +90 -0
- package/src/singleton/pattern.test.ts +16 -0
- package/src/singleton/pattern.ts +44 -0
- package/src/string-util.test.ts +18 -0
- package/src/string-util.ts +18 -0
- package/src/time-util.test.ts +89 -0
- package/src/time-util.ts +98 -0
- package/src/timeout.test.ts +65 -0
- package/src/timeout.ts +16 -0
- package/src/type-util.test.ts +20 -0
- package/src/type-util.ts +54 -0
- package/src/types/any-function/index.ts +1 -0
- package/src/types/any-function/no-params.ts +1 -0
- package/src/types/any-function/promise-no-params.ts +1 -0
- package/src/types/any-function/promise.ts +1 -0
- package/src/types/types.d.ts +2 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import cloneDeep from 'lodash.clonedeep'
|
|
2
|
+
import util from 'util'
|
|
3
|
+
|
|
4
|
+
export type ObjectType = Record<string, any>
|
|
5
|
+
|
|
6
|
+
export class ObjectUtil {
|
|
7
|
+
/**
|
|
8
|
+
* Deep clone object. Returned object will have no references to the object passed through params
|
|
9
|
+
* @template T
|
|
10
|
+
* @param {T} objectToClone
|
|
11
|
+
* @return {T}
|
|
12
|
+
*/
|
|
13
|
+
deepClone<T extends ObjectType>(objectToClone: T): T {
|
|
14
|
+
return cloneDeep(objectToClone)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pick only properties from the property list. It is only allowed to pick properties from the first level
|
|
19
|
+
* @template T
|
|
20
|
+
* @template L
|
|
21
|
+
* @param {T} obj
|
|
22
|
+
* @param {L[]} keyList
|
|
23
|
+
* @return {Pick<T, L>}
|
|
24
|
+
*/
|
|
25
|
+
pickByList<T extends object, L extends keyof T>(obj: T, keyList: (L | string)[]): Pick<T, L> {
|
|
26
|
+
return keyList.reduce((pickedObj, key) => {
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
28
|
+
pickedObj[key as L] = obj[key as L]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return pickedObj
|
|
32
|
+
}, {} as Pick<T, L>) // eslint-disable-line @typescript-eslint/prefer-reduce-type-parameter
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pick objects properties using keys from the second object.
|
|
37
|
+
* @template T
|
|
38
|
+
* @template L
|
|
39
|
+
* @param {T} obj
|
|
40
|
+
* @param {Partial<T>} objWithPickKeys
|
|
41
|
+
* @return {Pick<T, L>}
|
|
42
|
+
*/
|
|
43
|
+
pickByObjectKeys<T extends object, L extends keyof T>(obj: T, objWithPickKeys: Partial<T> | ObjectType): Pick<T, L> {
|
|
44
|
+
const keys = Object.keys(objWithPickKeys) as L[]
|
|
45
|
+
|
|
46
|
+
return this.pickByList<T, L>(obj, keys)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function will do stringify deeper that JSON.stringify.
|
|
51
|
+
* @param {any} entity - entity thant needs to be stringify
|
|
52
|
+
* @param {object} [options] - available options
|
|
53
|
+
* @param {boolean} [options.isSortable=false] - if object property should be sorted
|
|
54
|
+
* @param {boolean} [options.isPrettyPrinted=false] - if object and array properties should be printed in a new row
|
|
55
|
+
* @param {number} [options.prettyPrintCompactLevel=0] - if pretty print is on define the level of deepest children that are not
|
|
56
|
+
* going to be pretty. It doesn't matter if the siblings doesn't have the same depth.
|
|
57
|
+
* @return {string} - strung result
|
|
58
|
+
* @example
|
|
59
|
+
* console.log(new ObjectUtil().deepStringify(null)) // 'null'
|
|
60
|
+
* console.log(new ObjectUtil().deepStringify(undefined)) // 'undefined'
|
|
61
|
+
* console.log(new ObjectUtil().deepStringify({ a: 1 })) // '{\n\ta: 1\n}'
|
|
62
|
+
* // `{
|
|
63
|
+
* // a:1
|
|
64
|
+
* // }`
|
|
65
|
+
* console.log(new ObjectUtil().deepStringify({ b: 1, a: 2 }, {isSorted:true, compact: true})) // { a: 2, b: 1 }
|
|
66
|
+
*/
|
|
67
|
+
deepStringify(
|
|
68
|
+
entity: any,
|
|
69
|
+
options?: { isSorted?: boolean; isPrettyPrinted?: boolean; prettyPrintCompactLevel?: number }
|
|
70
|
+
): string {
|
|
71
|
+
const { isSorted = false, isPrettyPrinted = false, prettyPrintCompactLevel = 0 } = options ?? {}
|
|
72
|
+
|
|
73
|
+
const compact = this._deepStringifyCompact({ isPrettyPrinted, prettyPrintCompactLevel })
|
|
74
|
+
|
|
75
|
+
return util.inspect(entity, {
|
|
76
|
+
breakLength: Infinity,
|
|
77
|
+
compact,
|
|
78
|
+
depth: Infinity,
|
|
79
|
+
maxArrayLength: Infinity,
|
|
80
|
+
maxStringLength: Infinity,
|
|
81
|
+
sorted: isSorted,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected _deepStringifyCompact(params: { isPrettyPrinted: boolean; prettyPrintCompactLevel: number }): number | boolean {
|
|
86
|
+
const { isPrettyPrinted, prettyPrintCompactLevel } = params
|
|
87
|
+
|
|
88
|
+
if (!isPrettyPrinted) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return prettyPrintCompactLevel
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* We are converting objects to string (or null or undefined) and comparing if the result is equal
|
|
97
|
+
* @param a
|
|
98
|
+
* @param b
|
|
99
|
+
* @return {boolean}
|
|
100
|
+
*/
|
|
101
|
+
deepEqual(a: any, b: any): boolean {
|
|
102
|
+
return this.deepStringify(a, { isSorted: true }) === this.deepStringify(b, { isSorted: true })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function is going to convert any null to undefined in the object that is passed to it.
|
|
107
|
+
* @template T
|
|
108
|
+
* @param {T} objectWithNulls
|
|
109
|
+
* @return {T}
|
|
110
|
+
* @example
|
|
111
|
+
* console.log(new ObjectUtil().deepNullToUndefined({ a: null, b: { c: null } })) // { a: undefined, b: { c: undefined } }
|
|
112
|
+
*/
|
|
113
|
+
deepNullToUndefined<T extends ObjectType>(objectWithNulls: T): T {
|
|
114
|
+
return Object.entries(objectWithNulls).reduce<any>((acc, cur) => {
|
|
115
|
+
const [key, value] = cur
|
|
116
|
+
if (value === null) {
|
|
117
|
+
acc[key] = undefined
|
|
118
|
+
} else if (typeof value === 'object' && !(value instanceof Date)) {
|
|
119
|
+
acc[key] = this.deepNullToUndefined(value)
|
|
120
|
+
} else {
|
|
121
|
+
acc[key] = value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return acc
|
|
125
|
+
}, {})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { regexUtil } from 'src/regex-util'
|
|
2
|
+
|
|
3
|
+
describe('regexUtil', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['00000000-0000-0000-0000-000000000000'],
|
|
6
|
+
['b40c3094-a238-4c21-a744-f19b9e476abf'],
|
|
7
|
+
['7d39e0a8-4ca2-41ff-9ded-312fb08f4dda'],
|
|
8
|
+
['f9ea83de-c69f-42ff-8764-a6e88d59d75d'],
|
|
9
|
+
['33f13db5-d427-4ea4-b028-6fc30a84d827'],
|
|
10
|
+
['f04ec90e-2ff5-4b88-beb3-23b348b8e33b'],
|
|
11
|
+
['83ad915b-645a-4132-b8c1-ab398701ba66'],
|
|
12
|
+
['3510e9f1-08f6-4c5a-86a9-66e91c6093b7'],
|
|
13
|
+
['02f243d8-3225-460b-9df7-30380d96971d'],
|
|
14
|
+
['f27571d4-8c4a-4c9a-8899-932d0fa7a68c'],
|
|
15
|
+
['b87a9f9c-c7c2-4aa2-85af-87221cebce9d'],
|
|
16
|
+
])('%#. should pass regex expression check %s', (uuidString) => {
|
|
17
|
+
expect(new RegExp(regexUtil.uuid).test(uuidString)).toBeTruthy()
|
|
18
|
+
})
|
|
19
|
+
it.each([['a'], ['test'], [123], [{}], [{ uuid: '3510e9f1-08f6-4c5a-86a9-66e91c6093b7' }], [new Date()]])(
|
|
20
|
+
'%#. should pass regex expression check %s',
|
|
21
|
+
(uuidString) => {
|
|
22
|
+
expect(new RegExp(regexUtil.uuid).test(uuidString.toString())).toBeFalsy()
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const regexUtil = {
|
|
2
|
+
/**
|
|
3
|
+
* This is a UUID regex expression. This is usually used in express router to constrict the values passed as a path parameter (if you are using UUID as your identifier).
|
|
4
|
+
* @return {string}
|
|
5
|
+
* @example
|
|
6
|
+
* const { uuid } = regexUtil
|
|
7
|
+
* router.route(`/users/:userId(${uuid})`).get(getUsersById)
|
|
8
|
+
* //...
|
|
9
|
+
*/
|
|
10
|
+
uuid: `\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b` as const,
|
|
11
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { SingleThresholdPromise } from 'src/single-threshold-promise'
|
|
2
|
+
import { timeout } from 'src/timeout'
|
|
3
|
+
|
|
4
|
+
describe('SingleThresholdPromise', () => {
|
|
5
|
+
describe('promise', () => {
|
|
6
|
+
let callCounter = 0
|
|
7
|
+
const fake_asyncFactoryFn = jest.fn()
|
|
8
|
+
const fake_asyncRejectFactoryFn = jest.fn()
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
callCounter = 0
|
|
12
|
+
jest.useFakeTimers()
|
|
13
|
+
fake_asyncFactoryFn.mockImplementation(async (): Promise<{ callCount: number }> => {
|
|
14
|
+
await timeout(1000)
|
|
15
|
+
|
|
16
|
+
return { callCount: ++callCounter }
|
|
17
|
+
})
|
|
18
|
+
fake_asyncRejectFactoryFn.mockImplementation(async (): Promise<{ callCount: number }> => {
|
|
19
|
+
await timeout(1000)
|
|
20
|
+
throw new Error()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.clearAllTimers()
|
|
26
|
+
jest.useRealTimers()
|
|
27
|
+
jest.resetAllMocks()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return result of the factory function when promise called', async () => {
|
|
31
|
+
const singleThresholdPromiseImplementation = new SingleThresholdPromise(fake_asyncFactoryFn)
|
|
32
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
33
|
+
|
|
34
|
+
const promise = singleThresholdPromiseImplementation.promise()
|
|
35
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
36
|
+
jest.runAllTimers()
|
|
37
|
+
expect(await promise).toEqual({ callCount: 1 })
|
|
38
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should return the same promise result to multiple calls before the first promise is resolved ', async () => {
|
|
42
|
+
const expected = { callCount: 1 }
|
|
43
|
+
|
|
44
|
+
const singleThresholdPromiseImplementation = new SingleThresholdPromise(fake_asyncFactoryFn)
|
|
45
|
+
const promise1 = singleThresholdPromiseImplementation.promise()
|
|
46
|
+
const promise2 = singleThresholdPromiseImplementation.promise()
|
|
47
|
+
const promise3 = singleThresholdPromiseImplementation.promise()
|
|
48
|
+
|
|
49
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
50
|
+
jest.runAllTimers()
|
|
51
|
+
expect(await promise1).toEqual(expected)
|
|
52
|
+
expect(await promise2).toEqual(expected)
|
|
53
|
+
expect(await promise3).toEqual(expected)
|
|
54
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return different promise result to multiple calls if they are called after the promise is resolved', async () => {
|
|
58
|
+
const singleThresholdPromiseImplementation = new SingleThresholdPromise(fake_asyncFactoryFn)
|
|
59
|
+
|
|
60
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
61
|
+
|
|
62
|
+
const promise1 = singleThresholdPromiseImplementation.promise()
|
|
63
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
64
|
+
jest.runAllTimers()
|
|
65
|
+
expect(await promise1).toEqual({ callCount: 1 })
|
|
66
|
+
|
|
67
|
+
const promise2 = singleThresholdPromiseImplementation.promise()
|
|
68
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(2)
|
|
69
|
+
jest.runAllTimers()
|
|
70
|
+
expect(await promise2).toEqual({ callCount: 2 })
|
|
71
|
+
|
|
72
|
+
const promise3 = singleThresholdPromiseImplementation.promise()
|
|
73
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(3)
|
|
74
|
+
jest.runAllTimers()
|
|
75
|
+
expect(await promise3).toEqual({ callCount: 3 })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should reject all if promise is rejected', async () => {
|
|
79
|
+
const singleThresholdPromiseImplementation = new SingleThresholdPromise(fake_asyncRejectFactoryFn)
|
|
80
|
+
const promise1 = singleThresholdPromiseImplementation.promise()
|
|
81
|
+
const promise2 = singleThresholdPromiseImplementation.promise()
|
|
82
|
+
const promise3 = singleThresholdPromiseImplementation.promise()
|
|
83
|
+
expect(fake_asyncRejectFactoryFn).toHaveBeenCalledTimes(1)
|
|
84
|
+
jest.runAllTimers()
|
|
85
|
+
await promise1.then(() => expect.fail('test failed')).catch(() => undefined)
|
|
86
|
+
await promise2.then(() => expect.fail('test failed')).catch(() => undefined)
|
|
87
|
+
await promise3.then(() => expect.fail('test failed')).catch(() => undefined)
|
|
88
|
+
expect(fake_asyncRejectFactoryFn).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AnyFunctionPromiseNoParams } from 'src/types/any-function/promise-no-params'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SingleThresholdPromise returns a single promise, and subsequent calls made before the promise resolves will return the same promise.
|
|
5
|
+
* @example
|
|
6
|
+
* export const refreshTokenSingleThreshold = new SingleThresholdPromise(async () => {
|
|
7
|
+
* const oldRefreshToken = await refreshTokenService.get()
|
|
8
|
+
* const { accessToken, refreshToken } = await authService.refreshToken({
|
|
9
|
+
* refreshToken: oldRefreshToken,
|
|
10
|
+
* })
|
|
11
|
+
* return { accessToken, refreshToken }
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* export const authService = {
|
|
15
|
+
* refreshToken: async (): Promise<{ accessToken: string; refreshToken:string }> => {
|
|
16
|
+
* return refreshTokenSingleThreshold.promise()
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export class SingleThresholdPromise<T> {
|
|
21
|
+
protected _cache: {
|
|
22
|
+
promises?: { resolve: (value: T | PromiseLike<T>) => void; reject: (reason?: any) => void }[]
|
|
23
|
+
} = {}
|
|
24
|
+
|
|
25
|
+
protected _factoryFn: AnyFunctionPromiseNoParams<T>
|
|
26
|
+
|
|
27
|
+
constructor(factoryFn: AnyFunctionPromiseNoParams<T>) {
|
|
28
|
+
this._factoryFn = factoryFn
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected _rejectPromises(): void {
|
|
32
|
+
if (this._cache.promises) {
|
|
33
|
+
this._cache.promises.forEach((promise) => promise.reject(new Error('Cache was cleaned')))
|
|
34
|
+
}
|
|
35
|
+
delete this._cache.promises
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async promise(): Promise<T> {
|
|
39
|
+
if ('promises' in this._cache) {
|
|
40
|
+
return new Promise<T>((resolve, reject) => {
|
|
41
|
+
this._cache.promises!.push({ reject, resolve })
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._cache.promises = []
|
|
46
|
+
const result = await this._factoryFn().catch((err) => {
|
|
47
|
+
this._rejectPromises()
|
|
48
|
+
throw err
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
this._cache.promises.forEach((promise) => promise.resolve(result))
|
|
52
|
+
delete this._cache.promises
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SingletonAsync } from 'src/singleton/async'
|
|
2
|
+
import { timeout } from 'src/timeout'
|
|
3
|
+
|
|
4
|
+
describe('SingletonAsync', () => {
|
|
5
|
+
const fakeResult = { sucessful: true }
|
|
6
|
+
const fake_asyncFactoryFn = jest.fn()
|
|
7
|
+
const fake_asyncRejectFactoryFn = jest.fn()
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.useFakeTimers()
|
|
10
|
+
fake_asyncFactoryFn.mockImplementation(async (): Promise<{ sucessful: boolean }> => {
|
|
11
|
+
await timeout(1000)
|
|
12
|
+
|
|
13
|
+
return fakeResult
|
|
14
|
+
})
|
|
15
|
+
fake_asyncRejectFactoryFn.mockImplementation(async (): Promise<{ sucessful: boolean }> => {
|
|
16
|
+
await timeout(1000)
|
|
17
|
+
throw new Error()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
jest.clearAllTimers()
|
|
23
|
+
jest.useRealTimers()
|
|
24
|
+
jest.resetAllMocks()
|
|
25
|
+
})
|
|
26
|
+
describe('promise', () => {
|
|
27
|
+
it('should return promised value', async () => {
|
|
28
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
29
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
30
|
+
const promise = singletonImplementation.promise()
|
|
31
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
32
|
+
jest.runAllTimers()
|
|
33
|
+
expect(await promise).toBe(fakeResult)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should subscribe multiple calls to the same promise if promise still not resolved', async () => {
|
|
37
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
38
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
39
|
+
const promise1 = singletonImplementation.promise()
|
|
40
|
+
const promise2 = singletonImplementation.promise()
|
|
41
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
42
|
+
|
|
43
|
+
jest.runAllTimers()
|
|
44
|
+
expect(await promise1).toBe(fakeResult)
|
|
45
|
+
expect(await promise2).toBe(fakeResult)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should second call after promise is resolved should return cache value', async () => {
|
|
49
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
50
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
51
|
+
const promise1 = singletonImplementation.promise()
|
|
52
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
53
|
+
|
|
54
|
+
jest.runAllTimers()
|
|
55
|
+
expect(await promise1).toBe(fakeResult)
|
|
56
|
+
|
|
57
|
+
const promise2 = singletonImplementation.promise()
|
|
58
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
59
|
+
jest.runAllTimers()
|
|
60
|
+
expect(await promise2).toBe(fakeResult)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should reject all subscribers to promise if it is rejected', async () => {
|
|
64
|
+
const singletonImplementation = new SingletonAsync(fake_asyncRejectFactoryFn)
|
|
65
|
+
expect(fake_asyncRejectFactoryFn).not.toHaveBeenCalled()
|
|
66
|
+
const promise1 = singletonImplementation.promise()
|
|
67
|
+
const promise2 = singletonImplementation.promise()
|
|
68
|
+
expect(fake_asyncRejectFactoryFn).toHaveBeenCalledTimes(1)
|
|
69
|
+
|
|
70
|
+
jest.runAllTimers()
|
|
71
|
+
await promise1.then(() => expect.fail('test failed')).catch(() => undefined)
|
|
72
|
+
await promise2.then(() => expect.fail('test failed')).catch(() => undefined)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
describe('cached', () => {
|
|
76
|
+
it('should return undefined if promise is never called', () => {
|
|
77
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
78
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
79
|
+
expect(singletonImplementation.cached()).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
it('should cache result of the promise', async () => {
|
|
82
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
83
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
84
|
+
|
|
85
|
+
const promise = singletonImplementation.promise()
|
|
86
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
87
|
+
jest.runAllTimers()
|
|
88
|
+
expect(await promise).toBe(fakeResult)
|
|
89
|
+
expect(singletonImplementation.cached()).toBe(fakeResult)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
describe('cleanCache', () => {
|
|
93
|
+
it('should reject all subscribers to the promise if cleanCache is called before promise is resolved', async () => {
|
|
94
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
95
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
96
|
+
const promise1 = singletonImplementation.promise().catch(() => undefined)
|
|
97
|
+
const promise2 = singletonImplementation.promise().catch(() => undefined)
|
|
98
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
99
|
+
singletonImplementation.cleanCache()
|
|
100
|
+
jest.runAllTimers()
|
|
101
|
+
await promise1.then(() => expect.fail('test failed'))
|
|
102
|
+
await promise2.then(() => expect.fail('test failed'))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should clean cache and after the clean cache factory should be called again on promise', async () => {
|
|
106
|
+
const singletonImplementation = new SingletonAsync(fake_asyncFactoryFn)
|
|
107
|
+
expect(fake_asyncFactoryFn).not.toHaveBeenCalled()
|
|
108
|
+
const promise1 = singletonImplementation.promise()
|
|
109
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(1)
|
|
110
|
+
jest.runAllTimers()
|
|
111
|
+
expect(await promise1).toBe(fakeResult)
|
|
112
|
+
expect(singletonImplementation.cached()).toBe(fakeResult)
|
|
113
|
+
singletonImplementation.cleanCache()
|
|
114
|
+
expect(singletonImplementation.cached()).toBeUndefined()
|
|
115
|
+
|
|
116
|
+
const promise2 = singletonImplementation.promise()
|
|
117
|
+
expect(fake_asyncFactoryFn).toHaveBeenCalledTimes(2)
|
|
118
|
+
jest.runAllTimers()
|
|
119
|
+
expect(await promise2).toBe(fakeResult)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AnyFunctionPromiseNoParams } from 'src/types/any-function/promise-no-params'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is a singleton wrapper that is used to wrap around async function. We have additional functionality to clear the cache
|
|
5
|
+
* and reject any subscriptions to initial promise. And we can also check if there is anything i cache
|
|
6
|
+
* @example
|
|
7
|
+
* export const configSingleton = new SingletonAsync(async () => {
|
|
8
|
+
* await timeout(3000)
|
|
9
|
+
* return {
|
|
10
|
+
* env: process.env.NODE_ENV
|
|
11
|
+
* } as const
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* // using
|
|
15
|
+
* // cache value before we call promise
|
|
16
|
+
* console.log(configSingleton().cache()) // undefined
|
|
17
|
+
* console.log('NODE_ENV: ', await configSingleton().promise().env) // NODE_ENV: prod
|
|
18
|
+
* // cache value after we call promise
|
|
19
|
+
* console.log(configSingleton().cache()) // { env: 'prod' }
|
|
20
|
+
*/
|
|
21
|
+
export class SingletonAsync<T> {
|
|
22
|
+
protected _cache: {
|
|
23
|
+
singleton?: T
|
|
24
|
+
promises?: { resolve: (value: T | PromiseLike<T>) => void; reject: (reason?: any) => void }[]
|
|
25
|
+
} = {}
|
|
26
|
+
|
|
27
|
+
protected _factory: AnyFunctionPromiseNoParams<T>
|
|
28
|
+
|
|
29
|
+
constructor(factory: AnyFunctionPromiseNoParams<T>) {
|
|
30
|
+
this._factory = factory
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Empty cached value and reject any subscribed promise that is waiting for the initial promise to be resolved.
|
|
35
|
+
*/
|
|
36
|
+
cleanCache(): void {
|
|
37
|
+
delete this._cache.singleton
|
|
38
|
+
this._rejectPromises({ error: new Error('Cache was cleaned') })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected _rejectPromises(params: { error: Error }): void {
|
|
42
|
+
const { error } = params
|
|
43
|
+
|
|
44
|
+
if (this._cache.promises) {
|
|
45
|
+
this._cache.promises.forEach((promise) => promise.reject(error))
|
|
46
|
+
}
|
|
47
|
+
delete this._cache.promises
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return singleton value in a promise. If there is no cached value then try to get it from factory.
|
|
52
|
+
* @template T
|
|
53
|
+
* @returns {Promise<T>}
|
|
54
|
+
*/
|
|
55
|
+
async promise(): Promise<T> {
|
|
56
|
+
if ('singleton' in this._cache) {
|
|
57
|
+
return this._cache.singleton!
|
|
58
|
+
}
|
|
59
|
+
if ('promises' in this._cache) {
|
|
60
|
+
return new Promise<T>((resolve, reject) => {
|
|
61
|
+
this._cache.promises!.push({ reject, resolve })
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._cache.promises = []
|
|
66
|
+
const result = await this._factory().catch((error) => {
|
|
67
|
+
this._rejectPromises({ error })
|
|
68
|
+
throw error
|
|
69
|
+
})
|
|
70
|
+
this._cache.singleton = result
|
|
71
|
+
|
|
72
|
+
this._cache.promises.forEach((promise) => promise.resolve(result))
|
|
73
|
+
delete this._cache.promises
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return cached value, if there is no value cached return undefined.
|
|
80
|
+
* @template T
|
|
81
|
+
* @returns {T | undefined}
|
|
82
|
+
*/
|
|
83
|
+
cached(): T | undefined {
|
|
84
|
+
if ('singleton' in this._cache) {
|
|
85
|
+
return this._cache.singleton!
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { singletonPattern } from 'src/singleton/pattern'
|
|
2
|
+
|
|
3
|
+
describe('singletonPattern', () => {
|
|
4
|
+
it('should call factory function only once', () => {
|
|
5
|
+
const factoryResult = { successful: true }
|
|
6
|
+
const factoryFn = jest.fn().mockImplementation(() => {
|
|
7
|
+
return factoryResult
|
|
8
|
+
})
|
|
9
|
+
const singletonImplementation = singletonPattern(factoryFn)
|
|
10
|
+
expect(factoryFn).not.toHaveBeenCalled()
|
|
11
|
+
expect(singletonImplementation()).toBe(factoryResult)
|
|
12
|
+
expect(factoryFn).toHaveBeenCalledTimes(1)
|
|
13
|
+
expect(singletonImplementation()).toBe(factoryResult)
|
|
14
|
+
expect(factoryFn).toHaveBeenCalledTimes(1)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AnyFunctionNoParams } from 'src/types/any-function/no-params'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Singleton patter wrapper function
|
|
5
|
+
* @param {AnyFunctionNoParams<R>} factoryFn Factory function that is used to generate value that is going to be cached and return by
|
|
6
|
+
* singleton.
|
|
7
|
+
* @return {AnyFunctionNoParams<R>} Function result that returns cached value.
|
|
8
|
+
* @example
|
|
9
|
+
* export class SomeClass {
|
|
10
|
+
* constructor(protected _param: string){ }
|
|
11
|
+
* get param(): string {
|
|
12
|
+
* return this._param
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* export const someClassSingleton = singletonPattern((): SomeClass => {
|
|
16
|
+
* return new SomeClass('some param value')
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // using
|
|
20
|
+
* console.log('param: ', someClassSingleton().param) // param: some param value
|
|
21
|
+
*
|
|
22
|
+
* ///////////////////////////////////////////
|
|
23
|
+
* // Or we can use it with simple function //
|
|
24
|
+
* ///////////////////////////////////////////
|
|
25
|
+
* export const config = singletonPattern(() => {
|
|
26
|
+
* return {
|
|
27
|
+
* env: process.NODE_ENV,
|
|
28
|
+
* } as const
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* // using
|
|
32
|
+
* console.log('NODE_ENV: ', config().env) // NODE_ENV: prod
|
|
33
|
+
*/
|
|
34
|
+
export const singletonPattern = <R>(factoryFn: AnyFunctionNoParams<R>): AnyFunctionNoParams<R> => {
|
|
35
|
+
const cache: { singleton?: R } = {}
|
|
36
|
+
|
|
37
|
+
return (): R => {
|
|
38
|
+
if ('singleton' in cache) {
|
|
39
|
+
return cache.singleton!
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (cache.singleton = factoryFn())
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { regexUtil } from 'src/regex-util'
|
|
2
|
+
import { stringUtil } from 'src/string-util'
|
|
3
|
+
|
|
4
|
+
describe('stringUtil', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[stringUtil.generateUUID()],
|
|
7
|
+
[stringUtil.generateUUID()],
|
|
8
|
+
[stringUtil.generateUUID()],
|
|
9
|
+
[stringUtil.generateUUID()],
|
|
10
|
+
[stringUtil.generateUUID()],
|
|
11
|
+
[stringUtil.generateUUID()],
|
|
12
|
+
[stringUtil.generateUUID()],
|
|
13
|
+
[stringUtil.generateUUID()],
|
|
14
|
+
[stringUtil.generateUUID()],
|
|
15
|
+
])('%#. should generate valid uuid %s', (uuid) => {
|
|
16
|
+
expect(new RegExp(regexUtil.uuid).test(uuid)).toBeTruthy()
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const stringUtil = {
|
|
2
|
+
/**
|
|
3
|
+
* Generate random UUID
|
|
4
|
+
* @return {string}
|
|
5
|
+
* @example
|
|
6
|
+
* console.log(stringUtil.uuid()) // "69bfda25-df3f-46b4-8bbb-955cf5193426"
|
|
7
|
+
*/
|
|
8
|
+
generateUUID: (): string => {
|
|
9
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
10
|
+
const r = (Math.random() * 16) | 0
|
|
11
|
+
if (c == 'x') {
|
|
12
|
+
return r.toString(16)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return ((r & 0x3) | 0x8).toString(16)
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
}
|