make a new package for helpers
This commit is contained in:
25
packages/helpers/async-queue.ts
Normal file
25
packages/helpers/async-queue.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type AsyncCallback = () => unknown
|
||||
|
||||
export class AsyncQueue {
|
||||
private callbacks: AsyncCallback[] = []
|
||||
private promise: Promise<void> | undefined
|
||||
|
||||
async add(callback: AsyncCallback) {
|
||||
this.callbacks.push(callback)
|
||||
if (this.promise) return this.promise
|
||||
|
||||
this.promise = this.runQueue()
|
||||
try {
|
||||
await this.promise
|
||||
} finally {
|
||||
this.promise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async runQueue() {
|
||||
let callback: AsyncCallback | undefined
|
||||
while ((callback = this.callbacks.shift())) {
|
||||
await callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
packages/helpers/convert-object-property-case.test.ts
Normal file
42
packages/helpers/convert-object-property-case.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
CamelCasedPropertiesDeep,
|
||||
SnakeCasedPropertiesDeep,
|
||||
} from "type-fest"
|
||||
import { expect, test } from "vitest"
|
||||
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
|
||||
|
||||
test("camelCaseDeep", () => {
|
||||
const input = {
|
||||
some_prop: {
|
||||
some_deep_prop: "some_deep_value",
|
||||
},
|
||||
someOtherProp: "someOtherValue",
|
||||
}
|
||||
|
||||
const expected: CamelCasedPropertiesDeep<typeof input> = {
|
||||
someProp: {
|
||||
someDeepProp: "some_deep_value",
|
||||
},
|
||||
someOtherProp: "someOtherValue",
|
||||
}
|
||||
|
||||
expect(camelCaseDeep(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
test("snakeCaseDeep", () => {
|
||||
const input = {
|
||||
someProp: {
|
||||
someDeepProp: "someDeepValue",
|
||||
},
|
||||
some_other_prop: "someOtherValue",
|
||||
}
|
||||
|
||||
const expected: SnakeCasedPropertiesDeep<typeof input> = {
|
||||
some_prop: {
|
||||
some_deep_prop: "someDeepValue",
|
||||
},
|
||||
some_other_prop: "someOtherValue",
|
||||
}
|
||||
|
||||
expect(snakeCaseDeep(input)).toEqual(expected)
|
||||
})
|
||||
34
packages/helpers/convert-object-property-case.ts
Normal file
34
packages/helpers/convert-object-property-case.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { camelCase, isObject, snakeCase } from "lodash-es"
|
||||
import type {
|
||||
CamelCasedPropertiesDeep,
|
||||
SnakeCasedPropertiesDeep,
|
||||
} from "type-fest"
|
||||
|
||||
function convertKeyCaseDeep<Input, Output>(
|
||||
input: Input,
|
||||
convertKey: (key: string) => string,
|
||||
): Output {
|
||||
if (!isObject(input)) {
|
||||
return input as unknown as Output
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) =>
|
||||
convertKeyCaseDeep(item, convertKey),
|
||||
) as unknown as Output
|
||||
}
|
||||
|
||||
const output: any = {}
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
|
||||
return convertKeyCaseDeep(input, camelCase)
|
||||
}
|
||||
|
||||
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
|
||||
return convertKeyCaseDeep(input, snakeCase)
|
||||
}
|
||||
21
packages/helpers/generate-prop-combinations.ts
Normal file
21
packages/helpers/generate-prop-combinations.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function generatePropCombinations<P>(values: {
|
||||
[K in keyof P]: ReadonlyArray<P[K]>
|
||||
}) {
|
||||
return generatePropCombinationsRecursive(values) as P[]
|
||||
}
|
||||
|
||||
function generatePropCombinationsRecursive(
|
||||
value: Record<string, readonly unknown[]>,
|
||||
): Array<Record<string, unknown>> {
|
||||
const [key] = Object.keys(value)
|
||||
if (!key) return [{}]
|
||||
|
||||
const { [key]: values = [], ...otherValues } = value
|
||||
const result: Array<Record<string, unknown>> = []
|
||||
for (const value of values) {
|
||||
for (const otherValue of generatePropCombinationsRecursive(otherValues)) {
|
||||
result.push({ [key]: value, ...otherValue })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
5
packages/helpers/get-environment-value.ts
Normal file
5
packages/helpers/get-environment-value.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { raise } from "./raise.js"
|
||||
|
||||
export function getEnvironmentValue(name: string) {
|
||||
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
|
||||
}
|
||||
7
packages/helpers/is-instance-of.ts
Normal file
7
packages/helpers/is-instance-of.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* for narrowing instance types with array.filter
|
||||
*/
|
||||
export const isInstanceOf =
|
||||
<T>(Constructor: new (...args: any[]) => T) =>
|
||||
(value: unknown): value is T =>
|
||||
value instanceof Constructor
|
||||
7
packages/helpers/is-object.ts
Normal file
7
packages/helpers/is-object.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function isObject<T>(
|
||||
value: T,
|
||||
): value is Exclude<T, Primitive | AnyFunction> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
type Primitive = string | number | boolean | undefined | null
|
||||
type AnyFunction = (...args: any[]) => any
|
||||
3
packages/helpers/last.ts
Normal file
3
packages/helpers/last.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function last<T>(array: T[]): T | undefined {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
11
packages/helpers/log-pretty.ts
Normal file
11
packages/helpers/log-pretty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { inspect } from "node:util"
|
||||
|
||||
export function logPretty(value: unknown) {
|
||||
console.info(
|
||||
inspect(value, {
|
||||
// depth: Number.POSITIVE_INFINITY,
|
||||
depth: 10,
|
||||
colors: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
13
packages/helpers/omit.ts
Normal file
13
packages/helpers/omit.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function omit<Subject extends object, Key extends PropertyKey>(
|
||||
subject: Subject,
|
||||
keys: Key[],
|
||||
// hack: using a conditional type preserves union types
|
||||
): Subject extends any ? Omit<Subject, Key> : never {
|
||||
const result: any = {}
|
||||
for (const key in subject) {
|
||||
if (!keys.includes(key as unknown as Key)) {
|
||||
result[key] = subject[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
10
packages/helpers/package.json
Normal file
10
packages/helpers/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@reacord/helpers",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"type-fest": "^2.17.0"
|
||||
}
|
||||
}
|
||||
15
packages/helpers/pick.ts
Normal file
15
packages/helpers/pick.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LoosePick, UnknownRecord } from "./types"
|
||||
|
||||
export function pick<T, K extends keyof T | PropertyKey>(
|
||||
object: T,
|
||||
keys: K[],
|
||||
): LoosePick<T, K> {
|
||||
const result: any = {}
|
||||
for (const key of keys) {
|
||||
const value = (object as UnknownRecord)[key]
|
||||
if (value !== undefined) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
35
packages/helpers/prune-nullish-values.test.ts
Normal file
35
packages/helpers/prune-nullish-values.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "vitest"
|
||||
import type { PruneNullishValues } from "./prune-nullish-values"
|
||||
import { pruneNullishValues } from "./prune-nullish-values"
|
||||
|
||||
test("pruneNullishValues", () => {
|
||||
type InputType = {
|
||||
a: string
|
||||
b: string | null | undefined
|
||||
c?: string
|
||||
d: {
|
||||
a: string
|
||||
b: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const input: InputType = {
|
||||
a: "a",
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
b: null,
|
||||
c: undefined,
|
||||
d: {
|
||||
a: "a",
|
||||
b: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
const output: PruneNullishValues<InputType> = {
|
||||
a: "a",
|
||||
d: {
|
||||
a: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(pruneNullishValues(input)).toEqual(output)
|
||||
})
|
||||
42
packages/helpers/prune-nullish-values.ts
Normal file
42
packages/helpers/prune-nullish-values.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { isObject } from "./is-object"
|
||||
|
||||
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
||||
if (Array.isArray(input)) {
|
||||
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
|
||||
}
|
||||
|
||||
if (!isObject(input)) {
|
||||
return input as any
|
||||
}
|
||||
|
||||
const result: any = {}
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value != undefined) {
|
||||
result[key] = pruneNullishValues(value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export type PruneNullishValues<Input> = Input extends object
|
||||
? OptionalKeys<
|
||||
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
||||
KeysWithNullishValues<Input>
|
||||
>
|
||||
: Input
|
||||
|
||||
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
||||
[Key in Keys]?: Input[Key]
|
||||
}
|
||||
|
||||
type KeysWithNullishValues<Input> = NonNullable<
|
||||
Values<{
|
||||
[Key in keyof Input]: null extends Input[Key]
|
||||
? Key
|
||||
: undefined extends Input[Key]
|
||||
? Key
|
||||
: never
|
||||
}>
|
||||
>
|
||||
|
||||
type Values<Input> = Input[keyof Input]
|
||||
5
packages/helpers/raise.ts
Normal file
5
packages/helpers/raise.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { toError } from "./to-error.js"
|
||||
|
||||
export function raise(error: unknown): never {
|
||||
throw toError(error)
|
||||
}
|
||||
10
packages/helpers/reject-after.ts
Normal file
10
packages/helpers/reject-after.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import { toError } from "./to-error.js"
|
||||
|
||||
export async function rejectAfter(
|
||||
timeMs: number,
|
||||
error: unknown = `rejected after ${timeMs}ms`,
|
||||
): Promise<never> {
|
||||
await setTimeout(timeMs)
|
||||
throw toError(error)
|
||||
}
|
||||
21
packages/helpers/retry-with-timeout.ts
Normal file
21
packages/helpers/retry-with-timeout.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
|
||||
const maxTime = 500
|
||||
const waitPeriod = 50
|
||||
|
||||
export async function retryWithTimeout<T>(
|
||||
callback: () => Promise<T> | T,
|
||||
): Promise<T> {
|
||||
const startTime = Date.now()
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await callback()
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > maxTime) {
|
||||
throw error
|
||||
}
|
||||
await setTimeout(waitPeriod)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/helpers/to-error.ts
Normal file
3
packages/helpers/to-error.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function toError(value: unknown) {
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
}
|
||||
4
packages/helpers/to-upper.ts
Normal file
4
packages/helpers/to-upper.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/** A typesafe version of toUpperCase */
|
||||
export function toUpper<S extends string>(string: S) {
|
||||
return string.toUpperCase() as Uppercase<S>
|
||||
}
|
||||
11
packages/helpers/types.ts
Normal file
11
packages/helpers/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type MaybePromise<T> = T | Promise<T>
|
||||
|
||||
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
|
||||
? Value
|
||||
: Type[keyof Type]
|
||||
|
||||
export type UnknownRecord = Record<PropertyKey, unknown>
|
||||
|
||||
export type LoosePick<Shape, Keys extends PropertyKey> = {
|
||||
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never
|
||||
}
|
||||
21
packages/helpers/wait-for.ts
Normal file
21
packages/helpers/wait-for.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
|
||||
const maxTime = 1000
|
||||
|
||||
export async function waitFor<Result>(
|
||||
predicate: () => Result,
|
||||
): Promise<Awaited<Result>> {
|
||||
const startTime = Date.now()
|
||||
let lastError: unknown
|
||||
|
||||
while (Date.now() - startTime < maxTime) {
|
||||
try {
|
||||
return await predicate()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
await setTimeout(50)
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new Error("Timeout")
|
||||
}
|
||||
24
packages/helpers/with-logged-method-calls.ts
Normal file
24
packages/helpers/with-logged-method-calls.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { inspect } from "node:util"
|
||||
|
||||
export function withLoggedMethodCalls<T extends object>(value: T) {
|
||||
return new Proxy(value as Record<string | symbol, unknown>, {
|
||||
get(target, property) {
|
||||
const value = target[property]
|
||||
if (typeof value !== "function") {
|
||||
return value
|
||||
}
|
||||
return (...values: any[]) => {
|
||||
console.info(
|
||||
`${String(property)}(${values
|
||||
.map((value) =>
|
||||
typeof value === "object" && value !== null
|
||||
? value.constructor.name
|
||||
: inspect(value, { colors: true }),
|
||||
)
|
||||
.join(", ")})`,
|
||||
)
|
||||
return value.apply(target, values)
|
||||
}
|
||||
},
|
||||
}) as T
|
||||
}
|
||||
Reference in New Issue
Block a user