@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
370 lines (323 loc) • 9.08 kB
text/typescript
/// <reference lib="es2022" preserve="true" />
/// <reference lib="dom" preserve="true" />
import type { ErrorData } from '../error/error.model.js'
import type { CommonLogger } from '../log/commonLogger.js'
import type {
AnyObject,
NumberOfMilliseconds,
Promisable,
Reviver,
UnixTimestampMillis,
} from '../types.js'
import type { HttpMethod, HttpStatusFamily } from './http.model.js'
export interface FetcherNormalizedCfg
extends Required<FetcherCfg>,
Omit<
FetcherRequest,
| 'started'
| 'fullUrl'
| 'logRequest'
| 'logRequestBody'
| 'logResponse'
| 'logResponseBody'
| 'debug'
| 'redirect'
| 'credentials'
| 'throwHttpErrors'
| 'errorData'
> {
logger: CommonLogger
searchParams: Record<string, any>
}
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>
export type FetcherAfterResponseHook = <BODY = unknown>(
res: FetcherResponse<BODY>,
) => Promisable<void>
export type FetcherBeforeRetryHook = <BODY = unknown>(
res: FetcherResponse<BODY>,
) => Promisable<void>
/**
* Allows to mutate the error.
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
*/
export type FetcherOnErrorHook = (err: Error) => Promisable<void>
export interface FetcherCfg {
/**
* Should **not** contain trailing slash.
*/
baseUrl?: string
/**
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
* properties of hook function arguments.
* If you throw an error from the hook - it will be re-thrown as-is.
*/
hooks?: {
/**
* Allows to mutate req.
*/
beforeRequest?: FetcherBeforeRequestHook[]
/**
* Allows to mutate res.
* If you set `res.err` - it will be thrown.
*/
afterResponse?: FetcherAfterResponseHook[]
/**
* Allows to mutate res.retryStatus to override retry behavior.
*/
beforeRetry?: FetcherBeforeRetryHook[]
onError?: FetcherOnErrorHook[]
}
/**
* If Fetcher has an error - `errorData` object will be appended to the error data.
* Like this:
*
* _errorDataAppend(err, cfg.errorData)
*
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
*/
errorData?: ErrorData | undefined
/**
* If true - enables all possible logging.
*/
debug?: boolean
logRequest?: boolean
logRequestBody?: boolean
logResponse?: boolean
logResponseBody?: boolean
/**
* Controls if `baseUrl` should be included in logs (both success and error).
*
* Defaults to `true` on ServerSide and `false` on ClientSide.
*
* Reasoning.
*
* ClientSide often uses one main "backend host".
* Not including baseUrl improves Sentry error grouping.
*
* ServerSide often uses one Fetcher instance per 3rd-party API.
* Not including baseUrl can introduce confusion of "which API is it?".
*/
logWithBaseUrl?: boolean
/**
* Default to true.
* Set to false to strip searchParams from url when logging (both success and error)
*/
logWithSearchParams?: boolean
/**
* Defaults to `console`.
*/
logger?: CommonLogger
throwHttpErrors?: boolean
}
export interface FetcherRetryStatus {
retryAttempt: number
retryTimeout: NumberOfMilliseconds
retryStopped: boolean
}
export interface FetcherRetryOptions {
count: number
timeout: NumberOfMilliseconds
timeoutMax: NumberOfMilliseconds
timeoutMultiplier: number
}
export interface FetcherRequest
extends Omit<FetcherOptions, 'method' | 'headers' | 'baseUrl' | 'url'> {
/**
* inputUrl is only the part that was passed in the request,
* without baseUrl or searchParams.
*/
inputUrl: string
/**
* fullUrl includes baseUrl and searchParams.
*/
fullUrl: string
init: RequestInitNormalized
responseType: FetcherResponseType
timeoutSeconds: number
retry: FetcherRetryOptions
retryPost: boolean
retry3xx: boolean
retry4xx: boolean
retry5xx: boolean
started: UnixTimestampMillis
}
export interface FetcherGraphQLOptions extends FetcherOptions {
query: string
variables?: AnyObject
/**
* When querying singular entities, it may be convenient to specify 1st level object to unwrap.
* Example:
* {
* homePage: { ... }
* }
*
* unwrapObject: 'homePage'
*
* would return the contents of `{ ... }`
*/
unwrapObject?: string
}
export interface FetcherOptions {
method?: HttpMethod
/**
* If defined - this `url` will override the original given `url`.
* baseUrl (and searchParams) will still modify it.
*/
url?: string
baseUrl?: string
/**
* Default: 30.
*
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
* so both should finish within this single timeout (not each).
*/
timeoutSeconds?: number
/**
* Supports all the types that RequestInit.body supports.
*
* Useful when you want to e.g pass FormData.
*/
body?: Blob | BufferSource | FormData | URLSearchParams | string
/**
* Same as `body`, but also conveniently sets the
* Content-Type header to `text/plain`
*/
text?: string
/**
* Same as `body`, but:
* 1. JSON.stringifies the passed variable
* 2. Conveniently sets the Content-Type header to `application/json`
*/
json?: any
/**
* Same as `body`, but:
* 1. Transforms the passed plain js object into URLSearchParams and passes it to `body`
* 2. Conveniently sets the Content-Type header to `application/x-www-form-urlencoded`
*/
form?: FormData | URLSearchParams | AnyObject
credentials?: RequestCredentials
/**
* Default to 'follow'.
* 'error' would throw on redirect.
* 'manual' will not throw, but return !ok response with 3xx status.
*/
redirect?: RequestRedirect
// Removing RequestInit from options to simplify FetcherOptions interface.
// Will instead only add hand-picked useful options, such as `credentials`.
// init?: Partial<RequestInitNormalized>
headers?: Record<string, any>
responseType?: FetcherResponseType // default to 'json'
searchParams?: Record<string, any>
/**
* Default is 2 retries (3 tries in total).
* Pass `retry: { count: 0 }` to disable retries.
*/
retry?: Partial<FetcherRetryOptions>
/**
* Defaults to false.
* Set to true to allow retrying `post` requests.
*/
retryPost?: boolean
/**
* Defaults to false.
*/
retry3xx?: boolean
/**
* Defaults to false.
*/
retry4xx?: boolean
/**
* Defaults to true.
*/
retry5xx?: boolean
jsonReviver?: Reviver
logRequest?: boolean
logRequestBody?: boolean
logResponse?: boolean
logResponseBody?: boolean
/**
* If true - enables all possible logging.
*/
debug?: boolean
/**
* If provided - will be used instead of static `Fetcher.callNativeFetch`.
*/
fetchFn?: FetchFunction
/**
* Default to true.
* Set to false to not throw on `!Response.ok`, but simply return `Response.body` as-is (json parsed, etc).
*/
throwHttpErrors?: boolean
/**
* If Fetcher has an error - `errorData` object will be appended to the error data.
* Like this:
*
* _errorDataAppend(err, cfg.errorData)
*
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
*/
errorData?: ErrorData
/**
* Allows to mutate the error.
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
*/
onError?: FetcherOnErrorHook
}
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
method: HttpMethod
headers: Record<string, any>
}
export interface FetcherSuccessResponse<BODY = unknown> {
ok: true
err: undefined
fetchResponse: Response
body: BODY
req: FetcherRequest
statusCode: number
statusFamily?: HttpStatusFamily
retryStatus: FetcherRetryStatus
signature: string
}
export interface FetcherErrorResponse<BODY = unknown> {
ok: false
err: Error
fetchResponse?: Response
body?: BODY
req: FetcherRequest
statusCode?: number
statusFamily?: HttpStatusFamily
retryStatus: FetcherRetryStatus
signature: string
}
export type FetcherResponse<BODY = unknown> =
| FetcherSuccessResponse<BODY>
| FetcherErrorResponse<BODY>
export type FetcherResponseType =
| 'json'
| 'text'
| 'void'
| 'arrayBuffer'
| 'blob'
| 'readableStream'
/**
* Signature for the `fetch` function.
* Used to be able to override and provide a different implementation,
* e.g when mocking.
*/
export type FetchFunction = (url: string, init: RequestInitNormalized) => Promise<Response>
export type GraphQLResponse<DATA> = GraphQLSuccessResponse<DATA> | GraphQLErrorResponse
export interface GraphQLSuccessResponse<DATA> {
data: DATA
errors: never
}
export interface GraphQLErrorResponse {
data: never
errors: GraphQLFormattedError[]
}
/**
* Copy-pasted from `graphql` package, slimmed down.
* See: https://spec.graphql.org/draft/#sec-Errors
*/
export interface GraphQLFormattedError {
message: string
}