Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type utils and wait and retry from node-core #412

Merged
merged 5 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/app/universal-ts-utils/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ export * from './public/object/groupByUnique.js'
export * from './public/object/isEmpty.js'
export * from './public/object/pick.js'
export * from './public/object/transformToKebabCase.js'

// type
export * from './public/type/hasMessage.js'
export * from './public/type/isError.js'
export * from './public/type/isObject.js'
export * from './public/type/isStandardizedError.js'

export * from './public/waitAndRetry.js'

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ describe('copyWithoutNullish', () => {
e: {},
})

expect(result).toMatchSnapshot()
expect(result).toMatchInlineSnapshot(`
{
"a": "a",
"b": "",
"c": " ",
"e": {},
}
`)
})

it('Removes undefined fields', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { hasMessage } from './hasMessage.js'

describe('hasMessage', () => {
it('true for something with message', () => {
const obj = {
message: 'hello',
}

expect(hasMessage(obj)).toBe(true)
})

it('false for something without message', () => {
const obj = {
hello: 'world',
}

expect(hasMessage(obj)).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { isObject } from './isObject.js'

export const hasMessage = (maybe: unknown): maybe is { message: string } =>
isObject(maybe) && typeof maybe.message === 'string'
41 changes: 41 additions & 0 deletions packages/app/universal-ts-utils/src/public/type/isError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { isError } from './isError.js'

describe('isError', () => {
it('true for Error', () => {
const error = new Error('')

expect(isError(error)).toBe(true)
})

it('true for Error extension', () => {
class MyError extends Error {}
const error = new MyError('')

expect(isError(error)).toBe(true)
})

it('true for Error', () => {
const error = new Error('bam')

expect(isError(error)).toBe(true)
})

it('false for string', () => {
const error = 'bam'

expect(isError(error)).toBe(false)
})

it('false for a number', () => {
const error = 43

expect(isError(error)).toBe(false)
})

it('false for a plain object', () => {
const error = {}

expect(isError(error)).toBe(false)
})
})
2 changes: 2 additions & 0 deletions packages/app/universal-ts-utils/src/public/type/isError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isError = (maybeError: unknown): maybeError is Error =>
maybeError instanceof Error || Object.prototype.toString.call(maybeError) === '[object Error]'
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { isObject } from './isObject.js'

describe('isObject', () => {
it('true for object', () => {
const error = new Error('error')

expect(isObject(error)).toBe(true)
})

it('false for non-object', () => {
const error = 'error'

expect(isObject(error)).toBe(false)
})

it('false for null', () => {
const error = null

expect(isObject(error)).toBe(false)
})
})
2 changes: 2 additions & 0 deletions packages/app/universal-ts-utils/src/public/type/isObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isObject = (maybeObject: unknown): maybeObject is Record<PropertyKey, unknown> =>
typeof maybeObject === 'object' && maybeObject !== null
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { isStandardizedError } from './isStandardizedError.js'

describe('isStandardizedError', () => {
it('true for standardized error', () => {
const error = {
message: 'dummy',
code: 'code',
}

expect(isStandardizedError(error)).toBe(true)
})

it('false for non standardized error', () => {
const error = new Error()

expect(isStandardizedError(error)).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isObject } from './isObject.js'

// Error structure commonly used in libraries, e. g. fastify
export type StandardizedError = {
code: string
message: string
}

export const isStandardizedError = (error: unknown): error is StandardizedError =>
isObject(error) && typeof error.code === 'string' && typeof error.message === 'string'
70 changes: 70 additions & 0 deletions packages/app/universal-ts-utils/src/public/waitAndRetry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest'

import { waitAndRetry } from './waitAndRetry.js'

class Counter {
private readonly timeOfSucces: number
public executionCounter: number
constructor(msecsTillSuccess: number) {
this.timeOfSucces = Date.now() + msecsTillSuccess
this.executionCounter = 0
}

process() {
this.executionCounter++
return Date.now() >= this.timeOfSucces
}
}

describe('waitUtils', () => {
describe('waitAndRetry', () => {
it('executes once if there is an instant condition match', async () => {
const counter = new Counter(0)

const result = await waitAndRetry(() => {
return counter.process()
})

expect(result).toBe(true)
expect(counter.executionCounter).toBe(1)
})

it('executes until there is a condition match', async () => {
const counter = new Counter(1000)

const result = await waitAndRetry(
() => {
return counter.process()
},
50,
30,
)

expect(result).toBe(true)
expect(counter.executionCounter > 0).toBe(true)
})

it('times out of there is never a condition match', async () => {
const counter = new Counter(1000)

const result = await waitAndRetry(
() => {
return counter.process()
},
20,
30,
)

expect(result).toBe(false)
expect(counter.executionCounter > 0).toBe(true)
})

it('handles an error', async () => {
await expect(
waitAndRetry(() => {
throw new Error('it broke')
}),
).rejects.toThrowError('it broke')
})
})
})
33 changes: 33 additions & 0 deletions packages/app/universal-ts-utils/src/public/waitAndRetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const waitAndRetry = async <T>(
predicateFn: () => T,
sleepTime = 20,
maxRetryCount = 15,
): Promise<T> => {
return new Promise((resolve, reject) => {
let retryCount = 0

const performCheck = () => {
// amount of retries exceeded
if (maxRetryCount !== 0 && retryCount > maxRetryCount) {
resolve(predicateFn())
return
}

// Try executing predicateFn
Promise.resolve()
.then(() => predicateFn())
.then((result) => {
if (result) {
resolve(result)
return
}

retryCount++
setTimeout(performCheck, sleepTime)
})
.catch((err) => reject(err))
}

performCheck()
})
}
Loading