@atproto/common-web
Version:
Shared web-platform-friendly code for atproto libraries
181 lines (159 loc) • 4.19 kB
text/typescript
export const noUndefinedVals = <T>(
obj: Record<string, T | undefined>,
): Record<string, T> => {
Object.keys(obj).forEach((k) => {
if (obj[k] === undefined) {
delete obj[k]
}
})
return obj as Record<string, T>
}
export function aggregateErrors(
errors: unknown[],
message?: string,
): Error | AggregateError {
if (errors.length === 1) {
return errors[0] instanceof Error
? errors[0]
: new Error(message ?? stringifyError(errors[0]), { cause: errors[0] })
} else {
return new AggregateError(
errors,
message ?? `Multiple errors: ${errors.map(stringifyError).join('\n')}`,
)
}
}
function stringifyError(reason: unknown): string {
if (reason instanceof Error) {
return reason.message
}
return String(reason)
}
/**
* Returns a shallow copy of the object without the specified keys. If the input
* is nullish, it returns the input.
*/
export function omit<
T extends undefined | null | Record<string, unknown>,
K extends keyof NonNullable<T>,
>(
object: T,
rejectedKeys: readonly K[],
): T extends undefined ? undefined : T extends null ? null : Omit<T, K>
export function omit(
src: undefined | null | Record<string, unknown>,
rejectedKeys: readonly string[],
): undefined | null | Record<string, unknown> {
// Hot path
if (!src) return src
const dst = {}
const srcKeys = Object.keys(src)
for (let i = 0; i < srcKeys.length; i++) {
const key = srcKeys[i]
if (!rejectedKeys.includes(key)) {
dst[key] = src[key]
}
}
return dst
}
export const jitter = (maxMs: number) => {
return Math.round((Math.random() - 0.5) * maxMs * 2)
}
export const wait = (ms: number) => {
return new Promise((res) => setTimeout(res, ms))
}
export type BailableWait = {
bail: () => void
wait: () => Promise<void>
}
export const bailableWait = (ms: number): BailableWait => {
let bail
const waitPromise = new Promise<void>((res) => {
const timeout = setTimeout(res, ms)
bail = () => {
clearTimeout(timeout)
res()
}
})
return { bail, wait: () => waitPromise }
}
export const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => {
const length = arrs.reduce((acc, cur) => {
return acc + cur.length
}, 0)
const flattened = new Uint8Array(length)
let offset = 0
arrs.forEach((arr) => {
flattened.set(arr, offset)
offset += arr.length
})
return flattened
}
export const streamToBuffer = async (
stream: AsyncIterable<Uint8Array>,
): Promise<Uint8Array> => {
const arrays: Uint8Array[] = []
for await (const chunk of stream) {
arrays.push(chunk)
}
return flattenUint8Arrays(arrays)
}
const S32_CHAR = '234567abcdefghijklmnopqrstuvwxyz'
export const s32encode = (i: number): string => {
let s = ''
while (i) {
const c = i % 32
i = Math.floor(i / 32)
s = S32_CHAR.charAt(c) + s
}
return s
}
export const s32decode = (s: string): number => {
let i = 0
for (const c of s) {
i = i * 32 + S32_CHAR.indexOf(c)
}
return i
}
export const asyncFilter = async <T>(
arr: T[],
fn: (t: T) => Promise<boolean>,
) => {
const results = await Promise.all(arr.map((t) => fn(t)))
return arr.filter((_, i) => results[i])
}
export const isErrnoException = (
err: unknown,
): err is NodeJS.ErrnoException => {
return !!err && err['code']
}
export const errHasMsg = (err: unknown, msg: string): boolean => {
return !!err && typeof err === 'object' && err['message'] === msg
}
export const chunkArray = <T>(arr: T[], chunkSize: number): T[][] => {
return arr.reduce((acc, cur, i) => {
const chunkI = Math.floor(i / chunkSize)
if (!acc[chunkI]) {
acc[chunkI] = []
}
acc[chunkI].push(cur)
return acc
}, [] as T[][])
}
export const range = (num: number): number[] => {
const nums: number[] = []
for (let i = 0; i < num; i++) {
nums.push(i)
}
return nums
}
export const dedupeStrs = (strs: string[]): string[] => {
return [...new Set(strs)]
}
export const parseIntWithFallback = <T>(
value: string | undefined,
fallback: T,
): number | T => {
const parsed = parseInt(value || '', 10)
return isNaN(parsed) ? fallback : parsed
}