@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
172 lines (156 loc) • 5.51 kB
text/typescript
import {Observable, of, throwError} from 'rxjs'
import {filter, map} from 'rxjs/operators'
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
import {
type Any,
type ListenEvent,
type ListenEventName,
type ListenOptions,
type ListenParams,
type MutationEvent,
type OpenEvent,
type ReconnectEvent,
type WelcomeEvent,
} from '../types'
import defaults from '../util/defaults'
import {pick} from '../util/pick'
import {_getDataUrl} from './dataMethods'
import {encodeQueryString} from './encodeQueryString'
import {connectEventSource} from './eventsource'
import {eventSourcePolyfill} from './eventsourcePolyfill'
import {reconnectOnConnectionFailure} from './reconnectOnConnectionFailure'
// Limit is 16K for a _request_, eg including headers. Have to account for an
// unknown range of headers, but an average EventSource request from Chrome seems
// to have around 700 bytes of cruft, so let us account for 1.2K to be "safe"
const MAX_URL_LENGTH = 16000 - 1200
const possibleOptions = [
'includePreviousRevision',
'includeResult',
'includeMutations',
'includeAllVersions',
'visibility',
'effectFormat',
'tag',
]
const defaultOptions = {
includeResult: true,
}
/**
* Maps an array of listen events names to their corresponding listen event type, e.g:
* ```
* type Test = MapListenEventNamesToListenEvents<Doc, ['welcome']>
* // ^? WelcomeEvent
* ```
*
* @public
*/
export type MapListenEventNamesToListenEvents<
R extends Record<string, Any> = Record<string, Any>,
Events extends ListenEventName[] = ListenEventName[],
> = Events extends (infer E)[]
? E extends 'welcome'
? WelcomeEvent
: E extends 'mutation'
? MutationEvent<R>
: E extends 'reconnect'
? ReconnectEvent
: E extends 'open'
? OpenEvent
: never
: never
/**
* Maps a ListenOptions object and returns the Listen events opted for, e.g:
* ```
* type Test = ListenEventFromOptions<Doc, {events: ['welcome', 'mutation']}>
* // ^? WelcomeEvent | MutationEvent<Doc>
* ```
*
* @public
*/
export type ListenEventFromOptions<
R extends Record<string, Any> = Record<string, Any>,
Opts extends ListenOptions | undefined = undefined,
> = Opts extends ListenOptions
? Opts['events'] extends ListenEventName[]
? MapListenEventNamesToListenEvents<R, Opts['events']>
: // fall back to ListenEvent if opts events is present, but we can't infer the literal event names
ListenEvent<R>
: MutationEvent<R>
/**
* Set up a listener that will be notified when mutations occur on documents matching the provided query/filter.
*
* @param query - GROQ-filter to listen to changes for
* @param params - Optional query parameters
* @param options - Optional listener options
* @public
*/
export function _listen<R extends Record<string, Any> = Record<string, Any>>(
this: SanityClient | ObservableSanityClient,
query: string,
params?: ListenParams,
): Observable<MutationEvent<R>>
/**
* Set up a listener that will be notified when mutations occur on documents matching the provided query/filter.
*
* @param query - GROQ-filter to listen to changes for
* @param params - Optional query parameters
* @param options - Optional listener options
* @public
*/
export function _listen<
R extends Record<string, Any> = Record<string, Any>,
Opts extends ListenOptions = ListenOptions,
>(
this: SanityClient | ObservableSanityClient,
query: string,
params?: ListenParams,
options?: Opts,
): Observable<ListenEventFromOptions<R, Opts>>
/** @public */
export function _listen<
R extends Record<string, Any> = Record<string, Any>,
Opts extends ListenOptions = ListenOptions,
>(
this: SanityClient | ObservableSanityClient,
query: string,
params?: ListenParams,
opts: Opts = {} as Opts,
): Observable<ListenEventFromOptions<R, Opts>> {
const {url, token, withCredentials, requestTagPrefix, headers: configHeaders} = this.config()
const tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join('.') : opts.tag
const options = {...defaults(opts, defaultOptions), tag}
const listenOpts = pick(options, possibleOptions)
const qs = encodeQueryString({query, params, options: {tag, ...listenOpts}})
const uri = `${url}${_getDataUrl(this, 'listen', qs)}`
if (uri.length > MAX_URL_LENGTH) {
return throwError(() => new Error('Query too large for listener'))
}
const listenFor = (options.events ? options.events : ['mutation']) satisfies Opts['events']
const esOptions: EventSourceInit & {headers?: Record<string, string>} = {}
if (withCredentials) {
esOptions.withCredentials = true
}
if (token || configHeaders) {
esOptions.headers = {}
if (token) {
esOptions.headers.Authorization = `Bearer ${token}`
}
if (configHeaders) {
Object.assign(esOptions.headers, configHeaders)
}
}
const initEventSource = () =>
// use polyfill if there is no global EventSource or if we need to set headers
(typeof EventSource === 'undefined' || esOptions.headers
? eventSourcePolyfill
: of(EventSource)
).pipe(map((EventSource) => new EventSource(uri, esOptions)))
return connectEventSource(initEventSource, listenFor).pipe(
reconnectOnConnectionFailure(),
filter((event) => listenFor.includes(event.type)),
map((event) => ({
type: event.type,
...('data' in event ? (event.data as object) : {}),
})),
) as Observable<ListenEventFromOptions<R, Opts>>
}