make a new package for helpers

This commit is contained in:
itsMapleLeaf
2022-07-27 22:42:35 -05:00
parent 0df45acba3
commit 831bf9ea44
35 changed files with 276 additions and 49 deletions

View 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()
}
}
}

View 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)
})

View 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)
}

View 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
}

View File

@@ -0,0 +1,5 @@
import { raise } from "./raise.js"
export function getEnvironmentValue(name: string) {
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
}

View 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

View 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
View File

@@ -0,0 +1,3 @@
export function last<T>(array: T[]): T | undefined {
return array[array.length - 1]
}

View 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
View 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
}

View 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
View 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
}

View 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)
})

View 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]

View File

@@ -0,0 +1,5 @@
import { toError } from "./to-error.js"
export function raise(error: unknown): never {
throw toError(error)
}

View 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)
}

View 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)
}
}
}

View File

@@ -0,0 +1,3 @@
export function toError(value: unknown) {
return value instanceof Error ? value : new Error(String(value))
}

View 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
View 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
}

View 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")
}

View 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
}