@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.
@@ -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
+ }