@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
105 lines (97 loc) • 4.05 kB
text/typescript
import type {EventSourceFetchInit, FetchLikeResponse} from 'eventsource'
import type {FetchFunction, FetchInit} from 'get-it'
import type {InitializedClientConfig} from '../types'
/** @internal */
export interface EventSourceFetchOptions {
/**
* Headers that must be merged into every EventSource request. Used for
* the `Authorization` token, custom `headers` from the client config,
* etc. — things the native EventSource API has no equivalent for.
*/
headers?: Record<string, string>
/**
* If the client was configured with `withCredentials: true`, the
* resolved fetch forwards `credentials: 'include'` so the browser
* attaches cookies to the SSE request.
*/
withCredentials?: boolean
}
/**
* Build a `fetch` implementation suitable for the `eventsource` package's
* `fetch` option. Routes the EventSource connection through the same
* transport layer the rest of the client uses, so things like the
* test-fetch override, the per-request `proxy` config, and `HTTPS_PROXY`
* env-var support apply to SSE too.
*
* Resolution order on each request:
*
* 1. `config.resolveFetch(config.proxy)` if set — the client's fetch
* resolver, so SSE uses the same transport as regular requests:
* custom fetch variants (incl. the test suite's injected mock),
* undici configuration, an explicit `proxy` config, and env-proxy
* support all apply to SSE too. The Node entry supplies get-it's
* undici-backed fetch (threading the resolver through the env
* instead of importing `get-it/node` directly keeps `undici` out of
* the browser bundle); the browser entry leaves it unset.
* 2. `globalThis.fetch`. Note that Node's global fetch does NOT read
* proxy env vars (that is opt-in via `NODE_USE_ENV_PROXY`), which
* is one of the reasons step 1 exists.
*
* The returned fetch always merges `options.headers` into the outgoing
* request, regardless of which underlying fetch was picked.
*
* @internal
*/
export function resolveEventSourceFetch(
config: InitializedClientConfig,
options: EventSourceFetchOptions = {},
): EventSourceFetch {
const extraHeaders = options.headers
const credentials: FetchInit['credentials'] = options.withCredentials ? 'include' : undefined
return function eventSourceFetch(url, init) {
const baseFetch = pickBaseFetch(config)
// Extra `EventSourceFetchInit` fields get-it's `FetchInit` doesn't
// declare (`mode`, `cache`) survive the spread and reach whichever
// fetch implementation is effective.
const mergedInit: FetchInit = {...init}
if (extraHeaders) {
const headers = new Headers(init?.headers)
for (const [key, value] of Object.entries(extraHeaders)) {
headers.set(key, value)
}
mergedInit.headers = headers
}
if (credentials !== undefined) {
mergedInit.credentials = credentials
}
// get-it's `FetchResponse` is a structural superset of the package's
// `FetchLikeResponse`, so it can be handed over as-is.
return baseFetch(typeof url === 'string' ? url : url.href, mergedInit)
}
}
/**
* The fetch shape handed to the `eventsource` package: accepts what its
* `FetchLike` passes in, requires only what get-it's `FetchFunction`
* guarantees back — the full `typeof fetch` contract is not needed anywhere
* in this chain.
*
* @internal
*/
export type EventSourceFetch = (
url: string | URL,
init?: EventSourceFetchInit,
) => Promise<FetchLikeResponse>
/**
* The fetch the client's own transport resolves for this config: the
* configured `resolveFetch` (honouring an explicit `proxy`) if present,
* otherwise the global fetch. Shared by the EventSource connection and the
* `/check/cors` probe so both resolve identically.
*
* @internal
*/
export function pickBaseFetch(config: InitializedClientConfig): FetchFunction {
if (config.resolveFetch) {
return config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
}
return globalThis.fetch.bind(globalThis)
}