up-fetch
Version:
Advanced fetch client builder for typescript.
118 lines (105 loc) • 3.65 kB
text/typescript
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type {
DistributiveOmit,
HeadersObject,
JsonifiableArray,
JsonifiableObject,
KeyOf,
Params,
SerializeParams,
} from './types'
import { ValidationError } from './validation-error'
export const mergeHeaders = (
headerInits: (HeadersInit | HeadersObject | undefined)[],
) => {
const res: Record<string, string> = {}
headerInits.forEach((init) => {
// casting `init` to `HeadersInit` because `Record<string, any>` is
// properly transformed to `Record<string,string>` by `new Headers(init)`
new Headers(init as HeadersInit | undefined).forEach((value, key) => {
value === 'null' || value === 'undefined'
? delete res[key]
: (res[key] = value)
})
})
return res
}
export const withTimeout = (
signal: AbortSignal | undefined,
timeout: number | undefined,
): AbortSignal | undefined =>
// if AbortSignal.any is not supported
// AbortSignal.timeout is not supported either.
// Feature detection is fine on AbortSignal.any only
'any' in AbortSignal
? AbortSignal.any(
[signal, timeout && AbortSignal.timeout(timeout)].filter(
Boolean,
) as AbortSignal[],
)
: signal
export const omit = <O extends object, K extends KeyOf<O> | (string & {})>(
obj?: O,
keys: K[] | readonly K[] = [],
): DistributiveOmit<O, K> => {
const copy = { ...obj } as DistributiveOmit<O, K>
for (const key in copy) {
keys.includes(key as any) && delete copy[key]
}
return copy
}
export const isJsonifiable = (
value: any,
): value is JsonifiableObject | JsonifiableArray =>
isPlainObject(value) ||
Array.isArray(value) ||
typeof value?.toJSON === 'function'
const isPlainObject = (value: any): value is Record<string, any> =>
value && typeof value === 'object' && value.constructor?.name === 'Object'
export const resolveUrl = (
base: string | undefined = '',
input: URL | string,
defaultOptsParams: Params | undefined,
fetcherOptsParams: Params | undefined,
serializeParams: SerializeParams,
): string => {
input = (input as URL).href ?? input
const qs = serializeParams({
// Removing the 'url.searchParams.keys()' from the defaultParams
// but not from the 'fetcherParams'. The user is responsible for not
// specifying the params in both the "input" and the fetcher "params" option.
...omit(defaultOptsParams, [
...new URL(input, 'http://a').searchParams.keys(),
]),
...fetcherOptsParams,
})
let url: string = /^https?:\/\//.test(input)
? input
: !base || !input
? base + input
: base.replace(/\/$/, '') + '/' + input.replace(/^\//, '')
if (qs) {
url += (url.includes('?') ? '&' : '?') + qs.replace(/^\?/, '')
}
return url
}
export const abortableDelay = (delay: number, signal?: AbortSignal) =>
new Promise<void>((resolve, reject) => {
signal?.addEventListener('abort', handleAbort, { once: true })
const token = setTimeout(() => {
signal?.removeEventListener('abort', handleAbort)
resolve()
}, delay)
function handleAbort() {
clearTimeout(token)
reject(signal!.reason)
}
})
export async function validate<TSchema extends StandardSchemaV1>(
schema: TSchema,
data: StandardSchemaV1.InferInput<TSchema>,
): Promise<StandardSchemaV1.InferOutput<TSchema>> {
const result = await schema['~standard'].validate(data)
if (result.issues) throw new ValidationError(result, data)
return result.value
}